Compare commits
286 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
377ea183a7 | ||
|
|
72361ab8bf | ||
|
|
f708e583c3 | ||
|
|
d753e1dc48 | ||
|
|
315a8a3805 | ||
|
|
129fed9c9f | ||
|
|
0baccb7621 | ||
|
|
842a8aa45a | ||
|
|
d17843479c | ||
|
|
0d70cc8e58 | ||
|
|
4e6cacb206 | ||
|
|
52514ba35b | ||
|
|
4d59ce435e | ||
|
|
b3b7fa6f4d | ||
|
|
c057c16391 | ||
|
|
dee7cc6f2b | ||
|
|
3d0d87cb0c | ||
|
|
6b66d9b3f8 | ||
|
|
a301d94858 | ||
|
|
01199470f2 | ||
|
|
9e7ea19567 | ||
|
|
cdc6a6cb4a | ||
|
|
bb2d8fd7e0 | ||
|
|
2b6d418fe6 | ||
|
|
d8b1126b96 | ||
|
|
2eeddcfa80 | ||
|
|
59e3675a36 | ||
|
|
bc386f080d | ||
|
|
2dc1377154 | ||
|
|
610852e2f2 | ||
|
|
15b19bb8ce | ||
|
|
4a598b1837 | ||
|
|
caee2417d6 | ||
|
|
349ca36d4e | ||
|
|
ec86f5caf2 | ||
|
|
b5a5cdfcd2 | ||
|
|
72d799e065 | ||
|
|
d06f22dcd0 | ||
|
|
cb90630f27 | ||
|
|
59ad9204d0 | ||
|
|
cb97c16f5e | ||
|
|
69b48d5345 | ||
|
|
45ed4708c9 | ||
|
|
f3c77bdb3b | ||
|
|
dc0eb9eec1 | ||
|
|
83dd6443cb | ||
|
|
3d77f2d135 | ||
|
|
1ea219bddc | ||
|
|
39adba62d1 | ||
|
|
3526e84e04 | ||
|
|
bfdb706b60 | ||
|
|
a297e07055 | ||
|
|
56b4664ec7 | ||
|
|
70f7c75a92 | ||
|
|
e414b4de92 | ||
|
|
79e68f473f | ||
|
|
6656604809 | ||
|
|
85b4d11912 | ||
|
|
7769a23f59 | ||
|
|
c442f43090 | ||
|
|
d73670bf43 | ||
|
|
dd1967f0d0 | ||
|
|
e3d2fc64ac | ||
|
|
e07f20bf29 | ||
|
|
34f216181f | ||
|
|
8aef775474 | ||
|
|
f669ad92b6 | ||
|
|
cc0b272770 | ||
|
|
9ea6de340d | ||
|
|
be37f8a2a3 | ||
|
|
8a12fac39f | ||
|
|
0242fe12e3 | ||
|
|
acf2e1a5ec | ||
|
|
626db4be56 | ||
|
|
5941fa1ec7 | ||
|
|
1dd8651a1a | ||
|
|
33dd0ca16b | ||
|
|
dfe7852c25 | ||
|
|
abe6184f63 | ||
|
|
e8afb2143b | ||
|
|
7e1c363bad | ||
|
|
bcf6809deb | ||
|
|
bce5f6cf61 | ||
|
|
5495eebb38 | ||
|
|
d255801666 | ||
|
|
44f2601126 | ||
|
|
3a7366c4bc | ||
|
|
5da289714d | ||
|
|
a18d718744 | ||
|
|
c90fc461d9 | ||
|
|
e80fbe8934 | ||
|
|
e0650ade4f | ||
|
|
378b8458f2 | ||
|
|
182028d9ea | ||
|
|
0c3b8e7610 | ||
|
|
332fdcd2a7 | ||
|
|
aa20d04d3a | ||
|
|
949106bc09 | ||
|
|
67babc2858 | ||
|
|
d087ec510e | ||
|
|
c2ed3da87c | ||
|
|
f2d159f732 | ||
|
|
468fc2207d | ||
|
|
eced8bae82 | ||
|
|
e61ecb3963 | ||
|
|
304f4f8b2c | ||
|
|
48888087e1 | ||
|
|
75a70e70be | ||
|
|
cf825f912c | ||
|
|
1944a49fd8 | ||
|
|
657f343f5c | ||
|
|
74bb90b3d8 | ||
|
|
8bf9828c80 | ||
|
|
06324def38 | ||
|
|
70259a5ec5 | ||
|
|
a63057c594 | ||
|
|
48d5270611 | ||
|
|
712d0f3342 | ||
|
|
d6084aeca1 | ||
|
|
562b9624d7 | ||
|
|
a68d5e8bbe | ||
|
|
d45aa8197e | ||
|
|
314d3ef97a | ||
|
|
e3750ccd51 | ||
|
|
a712efe9d8 | ||
|
|
2266362e24 | ||
|
|
b7056b5baa | ||
|
|
8bd07bf56c | ||
|
|
569183efe9 | ||
|
|
937cf25e9b | ||
|
|
e2b6617577 | ||
|
|
8323d6394b | ||
|
|
e31b892a20 | ||
|
|
65b1518e26 | ||
|
|
865dbd3799 | ||
|
|
7de9d7967a | ||
|
|
aa2d2454e1 | ||
|
|
b850336872 | ||
|
|
138dec35c7 | ||
|
|
569fffa962 | ||
|
|
2a283e6793 | ||
|
|
73a7ba3ac9 | ||
|
|
c0c4ea9f86 | ||
|
|
a13179cd09 | ||
|
|
de089b7b73 | ||
|
|
1b700fb8e0 | ||
|
|
f0febf13f2 | ||
|
|
a002967a92 | ||
|
|
1167b20d89 | ||
|
|
297ff3ae90 | ||
|
|
a39e2ce15a | ||
|
|
78eda275d6 | ||
|
|
1fce0fd77d | ||
|
|
c942393f21 | ||
|
|
040cc30e73 | ||
|
|
6b75ffc928 | ||
|
|
a7f1b21f91 | ||
|
|
c8f7d9d5bc | ||
|
|
2520d45dc4 | ||
|
|
8bdf8d98c3 | ||
|
|
04025f3d32 | ||
|
|
4c7ed9c8ee | ||
|
|
c36f8b0df3 | ||
|
|
5ccec93940 | ||
|
|
14b15e18f3 | ||
|
|
3e6f3b4d80 | ||
|
|
470b3106cb | ||
|
|
e0074bc3ab | ||
|
|
8d04ecdc52 | ||
|
|
c83b1e88b9 | ||
|
|
0446cc499e | ||
|
|
8a6507e834 | ||
|
|
09893b9472 | ||
|
|
13d2290205 | ||
|
|
46e4c85563 | ||
|
|
d925036bd6 | ||
|
|
fbf2799674 | ||
|
|
12c7558b91 | ||
|
|
d55af76260 | ||
|
|
e4c70e2efb | ||
|
|
a971fee132 | ||
|
|
a45e0f78ef | ||
|
|
bf07dcb9ea | ||
|
|
e5f5b8f831 | ||
|
|
a80513fc50 | ||
|
|
f71de1742a | ||
|
|
57c65fdcda | ||
|
|
0a9cf5f9aa | ||
|
|
08fdf2bdad | ||
|
|
690c6bac38 | ||
|
|
af81308097 | ||
|
|
90b79f5c04 | ||
|
|
0c5dcec7bc | ||
|
|
a30dfbc15d | ||
|
|
52f3335977 | ||
|
|
c8b3e953ad | ||
|
|
e9d0526e1b | ||
|
|
408c3be675 | ||
|
|
f67f16733f | ||
|
|
25a173ad7b | ||
|
|
75ec88f7a7 | ||
|
|
e9c5ffb430 | ||
|
|
cb2cdaed12 | ||
|
|
029c7f1e2a | ||
|
|
5a8d6895fa | ||
|
|
bb11e23006 | ||
|
|
37b00d49c8 | ||
|
|
8055aed507 | ||
|
|
e7cef05c6a | ||
|
|
313746b578 | ||
|
|
a7c557222c | ||
|
|
9c902fb264 | ||
|
|
079f74d960 | ||
|
|
00d7de5276 | ||
|
|
3e928365de | ||
|
|
47ba174fb1 | ||
|
|
9446296daa | ||
|
|
7175a6fa7d | ||
|
|
1b06f7d317 | ||
|
|
c739bf6bfb | ||
|
|
dec9a72b41 | ||
|
|
593cbaa067 | ||
|
|
fb8906e371 | ||
|
|
df943250ac | ||
|
|
5a522a1489 | ||
|
|
0b0d64b9d0 | ||
|
|
18876e8a69 | ||
|
|
b668378e23 | ||
|
|
9de2c09a27 | ||
|
|
6b3d2bef12 | ||
|
|
344ed41bc7 | ||
|
|
2e711c3ac9 | ||
|
|
1bf4486cf1 | ||
|
|
cb116286ed | ||
|
|
7f0ae95dfb | ||
|
|
fe7ec9dcf5 | ||
|
|
d88eccdda3 | ||
|
|
78fe01d9a4 | ||
|
|
60cb41c76b | ||
|
|
af78f3bac4 | ||
|
|
fff86dcc8d | ||
|
|
5ec053ca34 | ||
|
|
e9f1631b06 | ||
|
|
3705993330 | ||
|
|
3db338da3e | ||
|
|
d126d0f5b8 | ||
|
|
549adebb30 | ||
|
|
40bada35c6 | ||
|
|
074903a299 | ||
|
|
328bee94e5 | ||
|
|
0db25f14f1 | ||
|
|
877e4f9416 | ||
|
|
8b3e864ffa | ||
|
|
5f5f677b7b | ||
|
|
3933d83d3e | ||
|
|
4abd35fb44 | ||
|
|
e68afb04eb | ||
|
|
bf2be96b29 | ||
|
|
b0b5048b01 | ||
|
|
aff69af690 | ||
|
|
c1d156cd6b | ||
|
|
e58e00be9d | ||
|
|
440fe972f4 | ||
|
|
e24588b961 | ||
|
|
57c8d69e83 | ||
|
|
79c0bebcf5 | ||
|
|
321c9c20d5 | ||
|
|
f6134b47da | ||
|
|
8c282b28a0 | ||
|
|
656cd11876 | ||
|
|
5e77c08872 | ||
|
|
1090f64117 | ||
|
|
470aaa29dc | ||
|
|
c6664af45b | ||
|
|
d6b0ce2565 | ||
|
|
770c9632ae | ||
|
|
cd60773d73 | ||
|
|
06cdd92129 | ||
|
|
315df33bd6 | ||
|
|
f990bda4e5 | ||
|
|
b755ad3602 | ||
|
|
b060b2827e | ||
|
|
73493b288f | ||
|
|
87640fb824 | ||
|
|
fb0a48f9db | ||
|
|
acc8670aa9 |
28
.github/dependabot.yml
vendored
28
.github/dependabot.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: github-actions
|
|
||||||
directory: /
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
groups:
|
|
||||||
actions:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
- package-ecosystem: cargo
|
|
||||||
directory: userspace/ksud
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
allow:
|
|
||||||
- dependency-type: "all"
|
|
||||||
groups:
|
|
||||||
crates:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
- package-ecosystem: gradle
|
|
||||||
directory: manager
|
|
||||||
schedule:
|
|
||||||
interval: daily
|
|
||||||
groups:
|
|
||||||
maven:
|
|
||||||
patterns:
|
|
||||||
- "*"
|
|
||||||
28
.github/workflows/build-lkm.yml
vendored
28
.github/workflows/build-lkm.yml
vendored
@@ -3,9 +3,9 @@ on:
|
|||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
upload:
|
upload:
|
||||||
required: false
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: "Whether to upload to branch"
|
description: "Whether to upload to branch"
|
||||||
secrets:
|
secrets:
|
||||||
# username:github_pat
|
# username:github_pat
|
||||||
@@ -14,9 +14,9 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
upload:
|
upload:
|
||||||
required: false
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: "Whether to upload to branch"
|
description: "Whether to upload to branch"
|
||||||
jobs:
|
jobs:
|
||||||
build-lkm:
|
build-lkm:
|
||||||
@@ -27,20 +27,20 @@ jobs:
|
|||||||
sub_level: 233
|
sub_level: 233
|
||||||
os_patch_level: 2025-02
|
os_patch_level: 2025-02
|
||||||
- version: "android13-5.10"
|
- version: "android13-5.10"
|
||||||
sub_level: 228
|
sub_level: 234
|
||||||
os_patch_level: 2025-01
|
os_patch_level: 2025-03
|
||||||
- version: "android13-5.15"
|
- version: "android13-5.15"
|
||||||
sub_level: 170
|
sub_level: 178
|
||||||
os_patch_level: 2025-01
|
os_patch_level: 2025-03
|
||||||
- version: "android14-5.15"
|
- version: "android14-5.15"
|
||||||
sub_level: 170
|
sub_level: 178
|
||||||
os_patch_level: 2025-01
|
os_patch_level: 2025-03
|
||||||
- version: "android14-6.1"
|
- version: "android14-6.1"
|
||||||
sub_level: 128
|
sub_level: 129
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-04
|
||||||
- version: "android15-6.6"
|
- version: "android15-6.6"
|
||||||
sub_level: 77
|
sub_level: 82
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-04
|
||||||
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||||
uses: ./.github/workflows/gki-kernel.yml
|
uses: ./.github/workflows/gki-kernel.yml
|
||||||
with:
|
with:
|
||||||
|
|||||||
32
.github/workflows/build-manager.yml
vendored
32
.github/workflows/build-manager.yml
vendored
@@ -8,7 +8,7 @@ on:
|
|||||||
- 'manager/**'
|
- 'manager/**'
|
||||||
- 'kernel/**'
|
- 'kernel/**'
|
||||||
- 'userspace/ksud/**'
|
- 'userspace/ksud/**'
|
||||||
- 'userspace/zakomksd/**'
|
- 'userspace/susfs/**'
|
||||||
- 'userspace/kpmmgr/**'
|
- 'userspace/kpmmgr/**'
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
@@ -29,7 +29,7 @@ on:
|
|||||||
upload_lkm:
|
upload_lkm:
|
||||||
required: true
|
required: true
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: true
|
||||||
description: "Whether to upload lkm"
|
description: "Whether to upload lkm"
|
||||||
jobs:
|
jobs:
|
||||||
check-build-lkm:
|
check-build-lkm:
|
||||||
@@ -64,7 +64,7 @@ jobs:
|
|||||||
cd ..
|
cd ..
|
||||||
rm -rf tmp
|
rm -rf tmp
|
||||||
fi
|
fi
|
||||||
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/susfs' ]; then
|
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
|
||||||
need_upload=true
|
need_upload=true
|
||||||
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
need_upload="${{ inputs.upload_lkm }}"
|
need_upload="${{ inputs.upload_lkm }}"
|
||||||
@@ -83,7 +83,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
build-zakomksd:
|
build-susfs:
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
needs: [ check-build-lkm, build-lkm ]
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
strategy:
|
strategy:
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
- target: aarch64-linux-android
|
- target: aarch64-linux-android
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
uses: ./.github/workflows/zakomksd.yml
|
uses: ./.github/workflows/susfs.yml
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
os: ${{ matrix.os }}
|
os: ${{ matrix.os }}
|
||||||
@@ -119,6 +119,8 @@ jobs:
|
|||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
- target: x86_64-linux-android
|
- target: x86_64-linux-android
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
- target: armv7-linux-androideabi
|
||||||
|
os: ubuntu-latest
|
||||||
uses: ./.github/workflows/ksud.yml
|
uses: ./.github/workflows/ksud.yml
|
||||||
with:
|
with:
|
||||||
target: ${{ matrix.target }}
|
target: ${{ matrix.target }}
|
||||||
@@ -174,10 +176,10 @@ jobs:
|
|||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
|
|
||||||
- name: Download arm64 zakomksd
|
- name: Download arm64 susfs
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zakomksd-aarch64-linux-android
|
name: susfs-aarch64-linux-android
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
- name: Download arm64 kpmmgr
|
- name: Download arm64 kpmmgr
|
||||||
@@ -198,22 +200,30 @@ jobs:
|
|||||||
name: ksud-x86_64-linux-android
|
name: ksud-x86_64-linux-android
|
||||||
path: .
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-armv7-linux-androideabi
|
||||||
|
path: .
|
||||||
|
|
||||||
- name: Copy ksud to app jniLibs
|
- name: Copy ksud to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
mkdir -p app/src/main/jniLibs/x86_64
|
mkdir -p app/src/main/jniLibs/x86_64
|
||||||
cp -f ../aarch64-linux-android/release/zakomk ../manager/app/src/main/jniLibs/arm64-v8a/libzakomk.so
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
cp -f ../x86_64-linux-android/release/zakomk ../manager/app/src/main/jniLibs/x86_64/libzakomk.so
|
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
|
- name: Copy kpmmgr to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
||||||
|
|
||||||
- name: Copy zakomksd to app jniLibs
|
- name: Copy susfs to app jniLibs
|
||||||
run: |
|
run: |
|
||||||
mkdir -p app/src/main/jniLibs/arm64-v8a
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
cp -f ../arm64-v8a/zakomksd ../manager/app/src/main/jniLibs/arm64-v8a/libzakomksd.so
|
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
|
||||||
|
|
||||||
- name: Build with Gradle
|
- name: Build with Gradle
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.github/workflows/gki-kernel.yml
vendored
3
.github/workflows/gki-kernel.yml
vendored
@@ -198,6 +198,9 @@ jobs:
|
|||||||
- name: Make working directory clean to avoid dirty
|
- name: Make working directory clean to avoid dirty
|
||||||
working-directory: android-kernel
|
working-directory: android-kernel
|
||||||
run: |
|
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!"
|
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||||
git config --global user.email "bot@kernelsu.org"
|
git config --global user.email "bot@kernelsu.org"
|
||||||
git config --global user.name "KernelSUBot"
|
git config --global user.name "KernelSUBot"
|
||||||
|
|||||||
2
.github/workflows/ksud.yml
vendored
2
.github/workflows/ksud.yml
vendored
@@ -71,4 +71,4 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ksud-${{ inputs.target }}
|
name: ksud-${{ inputs.target }}
|
||||||
path: userspace/ksud/target/**/release/zakomk*
|
path: userspace/ksud/target/**/release/zakozako*
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
name: Build zakomksd
|
name: Build susfs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "mian" ]
|
branches: [ "mian" ]
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/zakomksd.yml'
|
- '.github/workflows/susfs.yml'
|
||||||
- 'userspace/zakomksd/**'
|
- 'userspace/susfs/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
@@ -19,7 +19,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-susfs:
|
build-susfs:
|
||||||
name: Build userspace zakomksd
|
name: Build userspace susfs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -28,13 +28,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Build zakomksd
|
- name: Build susfs
|
||||||
working-directory: ./userspace/zakomksd
|
working-directory: ./userspace/susfs
|
||||||
run: |
|
run: |
|
||||||
$ANDROID_NDK_HOME/ndk-build
|
$ANDROID_NDK_HOME/ndk-build
|
||||||
|
|
||||||
- name: Upload a Build Artifact
|
- name: Upload a Build Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: zakomksd-aarch64-linux-android
|
name: susfs-aarch64-linux-android
|
||||||
path: ./userspace/zakomksd/libs
|
path: ./userspace/susfs/libs
|
||||||
@@ -1,101 +1,136 @@
|
|||||||
# SukiSU
|
# SukiSU Ultra
|
||||||
|
|
||||||
**Enlish** | [简体中文](README.md)
|
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
||||||
|
|
||||||
|
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
||||||
|
|
||||||
Android device root solution based on [KernelSU](https://github.com/KernelSU/KernelSU)
|
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
|
||||||
|
|
||||||
**Experimental! Use at your own risk! **This solution is based on [KernelSU]() and is experimental!
|
|
||||||
|
|
||||||
|
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
|
||||||
>
|
>
|
||||||
> This is an unofficial fork, all rights reserved [@tiann](https://github.com/tiann)
|
> 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
|
## How to add
|
||||||
|
|
||||||
Using the susfs-dev branch (integrated susfs with support for non-GKI devices)
|
Using main branching (non-GKI device builds are not supported)
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
Use main branching (no longer with support for non-GKI devices)
|
Using branches that support non-GKI devices
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
```
|
```
|
||||||
|
|
||||||
## How to use integrated susfs
|
## How to use integrated susfs
|
||||||
|
|
||||||
Use the susfs-dev branch directly without any patching
|
1. Use the susfs-dev branch directly without any patching
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
We will introduce more APatch-compatible functions to ensure the completeness of KPM functionality.
|
||||||
|
|
||||||
|
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
|
## More links
|
||||||
|
|
||||||
Projects compiled based on Sukisu and susfs
|
Projects compiled based on Sukisu and susfs
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
## Hook method
|
## Hook method
|
||||||
- This method references the hook manual to (https://github.com/rsuntk/KernelSU)
|
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
||||||
|
|
||||||
1. **KPROBES hook:**
|
1. **KPROBES hook:**
|
||||||
- This fork only supports GKI (5.10 - 6.x) kernels, all non-GKI kernels must use manual hooks.
|
- Also used for Loadable Kernel Module (LKM)
|
||||||
- For Loadable Kernel Modules (LKM)
|
- Default hook method on GKI kernels.
|
||||||
- Default hooking method for GKI kernels
|
- Need `CONFIG_KPROBES=y`
|
||||||
- Requires `CONFIG_KPROBES=y`. 2.
|
|
||||||
2. **Hooks manual:**
|
|
||||||
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
|
|
||||||
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
|
||||||
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
|
|
||||||
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
|
|
||||||
|
|
||||||
|
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
|
## Usage
|
||||||
[GKI]
|
|
||||||
1. such as millet redmi samsung and other devices (does not include the magic kernel manufacturers such as: meizu, a plus real me oppo)
|
|
||||||
2. find more links in the GKI build project to find the device kernel version directly download with TWRP or kernel flashing tool to brush into the zip with AnyKernel3 suffix can be
|
|
||||||
3. General without the suffix of the .zip compressed package is universal, gz suffix for the special TianGui models, lz4 suffix for Google models, general brush without the suffix can be!
|
|
||||||
|
|
||||||
[OnePlus]
|
### Universal GKI
|
||||||
1. Find the Yiga project in the More link and fill in your own, then build it with cloud compilation, and finally brush in the zip with AnyKernel3 suffix.
|
|
||||||
Note: You only need to fill in the first two kernel versions, such as 5.10, 5.15, 6.1, 6.6.
|
Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
|
||||||
- Please search for the processor codename by yourself, usually it is all English without numbers.
|
|
||||||
- Branching and configuration files, please fill in the kernel open source address.
|
> [!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.
|
||||||
|
> 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.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
1. Kernel-based `su` and root access management.
|
1. Kernel-based `su` and root access management.
|
||||||
2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system. 3.
|
2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system, but based on [Magic Mount](https://github.com/5ec1cff/KernelSU) from 5ec1cff
|
||||||
3. [Application Profiles](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage. 4.
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage.
|
||||||
4. Bringing back non-GKI/GKI 1.0 support
|
4. Bringing back non-GKI/GKI 1.0 support
|
||||||
5. More customization
|
5. More customization
|
||||||
|
6. Support for KPM kernel modules
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
- The file in the “kernel” directory is [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
|
- 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 [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html).
|
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
## Sponsorship list
|
## Sponsorship list
|
||||||
|
|
||||||
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
||||||
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
||||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
- [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!
|
||||||
|
|
||||||
|
|
||||||
How the above list does not have your name, I will keep you updated, thanks again for your support!
|
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
- [KernelSU](https://github.com/tiann/KernelSU): original project
|
- [KernelSU](https://github.com/tiann/KernelSU): original project
|
||||||
- [MKSU](https://github.com/5ec1cff/KernelSU): Used project
|
- [MKSU](https://github.com/5ec1cff/KernelSU): Used project
|
||||||
- [RKSU](https://github.com/rsuntk/KernelsU):Re-support of non-GKI devices using the kernel of this project
|
- [RKSU](https://github.com/rsuntk/KernelsU): Reintroduced the support of non-GKI devices using the kernel of this project
|
||||||
- [susfs](https://gitlab.com/simonpunk/susfs4ksu):Used susfs file system
|
- [susfs](https://gitlab.com/simonpunk/susfs4ksu):Used susfs file system
|
||||||
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU conceptualization
|
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU conceptualization
|
||||||
- [Magisk](https://github.com/topjohnwu/Magisk): Powerful root utility
|
- [Magisk](https://github.com/topjohnwu/Magisk): Powerful root utility
|
||||||
- [genuine](https://github.com/brevent/genuine/): APK v2 Signature Verification
|
- [genuine](https://github.com/brevent/genuine/): APK v2 Signature Verification
|
||||||
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills.
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit utilities.
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch is a key part of the APatch implementation of the kernel module
|
||||||
136
docs/README-ja.md
Normal file
136
docs/README-ja.md
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# SukiSU Ultra
|
||||||
|
|
||||||
|
**日本語** | [简体中文](README.md) | [English](README-en.md)
|
||||||
|
|
||||||
|
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
||||||
|
|
||||||
|
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||||
|
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||||
|
|
||||||
|
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||||
|
>
|
||||||
|
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||||
|
|
||||||
|
## 追加方法
|
||||||
|
|
||||||
|
メイン分岐の使用(GKI デバイス以外のビルドはサポートされていません。)
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
GKI以外のデバイスをサポートするブランチを使用する
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
|
## 統合された susfs の使い方
|
||||||
|
|
||||||
|
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## KPM に対応
|
||||||
|
|
||||||
|
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
||||||
|
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
||||||
|
|
||||||
|
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
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カーネルzipファイルを選択し、フラッシュするためにシステムの現在の実行スロットと反対のスロットを選択し、GKIモードアップデートを保持するために再起動します(この方法は、現時点ではすべてのnon_GKIデバイスでサポートされていませんので、各自でお試しください。 (この方法は、すべての非GKIデバイスでサポートされていませんので、ご自身でお試しください)。
|
||||||
|
- または、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 をベースにコンパイルされたプロジェクトです。
|
||||||
|
|
||||||
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_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
|
||||||
|
|
||||||
|
https://kernelsu.org/zh_CN/guide/installation.html をご参照ください。
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> 1.Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス用 (Meizu、Yiga、Zenith、oppo などのマジックカーネルを搭載したメーカーは除く)。
|
||||||
|
> 2. [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5) で GKI ビルドを検索します。 デバイスのカーネルバージョンを検索します。 次に、それをダウンロードし、TWRPまたはカーネルフラッシングツールを使用して、AnyKernel3の接尾辞が付いたzipファイルをフラッシュします。
|
||||||
|
> 接尾辞なしの.zipアーカイブは非圧縮で、接尾辞gzはTenguetモデルで使用されている圧縮方法です。
|
||||||
|
### OnePlus
|
||||||
|
|
||||||
|
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
|
||||||
|
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
|
||||||
|
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
|
||||||
|
|
||||||
|
## 機能
|
||||||
|
|
||||||
|
1. カーネルベースな `su` および root アクセスの管理。
|
||||||
|
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) モジュールシステムではなく、 5ec1cff 氏の [Magic Mount](https://github.com/5ec1cff/KernelSU) に基づいています。
|
||||||
|
3. [アプリプロファイル](https://kernelsu.org/guide/app-profile.html): root 権限をケージ内にロックします。
|
||||||
|
4. 非 GKI / GKI 1.0 の対応を復活
|
||||||
|
5. その他のカスタマイズ
|
||||||
|
6. KPM カーネルモジュールに対応
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。
|
||||||
|
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。
|
||||||
|
|
||||||
|
## スポンサーシップの一覧
|
||||||
|
|
||||||
|
- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。
|
||||||
|
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう!
|
||||||
|
|
||||||
|
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。
|
||||||
|
|
||||||
|
## 貢献者
|
||||||
|
|
||||||
|
- [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 実装での重要な部分となります。
|
||||||
100
docs/README.md
100
docs/README.md
@@ -1,80 +1,115 @@
|
|||||||
# SukiSU
|
# SukiSU Ultra
|
||||||
|
|
||||||
**简体中文** | [English](README-en.md)
|
**简体中文** | [English](README-en.md) | [日本語](README-ja.md)
|
||||||
|
|
||||||
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
||||||
|
|
||||||
**实验性!使用风险自负!**
|
**实验性! 使用风险自负!**
|
||||||
|
|
||||||
|
|
||||||
>
|
|
||||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
||||||
>
|
>
|
||||||
|
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
|
||||||
|
|
||||||
## 如何添加
|
## 如何添加
|
||||||
|
|
||||||
在内核源码的根目录下执行以下命令:
|
在内核源码的根目录下执行以下命令:
|
||||||
|
|
||||||
使用 susfs-dev 分支(已集成susfs,带非GKI设备的支持)
|
使用 main 分支 (不支持非GKI设备构建)
|
||||||
```
|
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
使用 main 分支(不再带非GKI设备的支持)
|
|
||||||
```
|
```
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
```
|
```
|
||||||
|
|
||||||
|
使用支持非 GKI 设备的分支
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
## 如何集成 susfs
|
## 如何集成 susfs
|
||||||
|
|
||||||
1. 直接使用 susfs-dev 分支,不需要再集成 susfs
|
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
||||||
|
```
|
||||||
|
|
||||||
## 钩子方法
|
## 钩子方法
|
||||||
|
|
||||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
||||||
|
|
||||||
1. **KPROBES 钩子:**
|
1. **KPROBES 钩子:**
|
||||||
- 此方法仅支持 GKI 2.0(5.10 - 6.x)内核,所有非 GKI 2.0 内核都必须使用手动钩子
|
|
||||||
- 用于可加载内核模块 (LKM)
|
- 用于可加载内核模块 (LKM)
|
||||||
- GKI 2.0 内核的默认钩子方法
|
- GKI 2.0 内核的默认钩子方法
|
||||||
- 需要 `CONFIG_KPROBES=y`
|
- 需要 `CONFIG_KPROBES=y`
|
||||||
|
|
||||||
2. **手动钩子:**
|
2. **手动钩子:**
|
||||||
- 对于 GKI 2.0(5.10 - 6.x)内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
|
|
||||||
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
- 标准的 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
|
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
||||||
- 部分手动集成KPROBES的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
- 非 GKI 内核的默认挂钩方法
|
||||||
|
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||||
|
|
||||||
|
## KPM 支持
|
||||||
|
|
||||||
|
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持
|
||||||
|
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
|
||||||
|
|
||||||
|
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
## 更多链接
|
## 更多链接
|
||||||
|
|
||||||
基于 SukiSU 和 susfs 编译的项目
|
基于 SukiSU 和 susfs 编译的项目
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
|
|
||||||
## 使用方法
|
## 使用方法
|
||||||
|
|
||||||
### GKI
|
### 普适的 GKI
|
||||||
1. 适用于如小米红米三星等的 GKI 2.0 的设备(不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
|
||||||
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
|
||||||
3. 一般不带后缀的 .zip 压缩包是通用,gz 后缀的为天玑机型专用,lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
|
|
||||||
|
> [!Note]
|
||||||
|
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
||||||
|
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
||||||
|
> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式
|
||||||
|
|
||||||
|
|
||||||
### 一加
|
### 一加
|
||||||
|
|
||||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
||||||
|
|
||||||
注意事项:
|
> [!Note]
|
||||||
- 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
||||||
- 处理器代号请自行搜索,一般为全英文不带数字的代号
|
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
||||||
- 分支和配置文件请自行到一加内核开源地址进行填写
|
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
||||||
|
|
||||||
|
|
||||||
## 特点
|
## 特点
|
||||||
|
|
||||||
1. 基于内核的 `su` 和 root 访问管理
|
1. 基于内核的 `su` 和 root 访问管理
|
||||||
2. 基于 5ec1cff 的 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统
|
2. 基于 5ec1cff 的 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统
|
||||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
||||||
4. 恢复对非 GKI 2.0 内核的支持(仅限susfs-dev和未进行susfs补丁的dev分支)
|
4. 恢复对非 GKI 2.0 内核的支持
|
||||||
5. 更多自定义功能
|
5. 更多自定义功能
|
||||||
|
6. 对 KPM 内核模块的支持
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
@@ -82,14 +117,14 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
|||||||
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||||
|
|
||||||
## 赞助名单
|
## 赞助名单
|
||||||
|
|
||||||
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
||||||
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
||||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||||
|
|
||||||
|
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
||||||
|
|
||||||
|
|
||||||
如何以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
|
||||||
|
|
||||||
## 贡献
|
## 贡献
|
||||||
|
|
||||||
@@ -101,3 +136,4 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
|
|||||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
||||||
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
||||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分
|
||||||
@@ -24,11 +24,12 @@ config KSU_HOOK
|
|||||||
override the kernel version check and enable the hook functionality.
|
override the kernel version check and enable the hook functionality.
|
||||||
|
|
||||||
config KPM
|
config KPM
|
||||||
bool "Enable KernelSU KPM"
|
bool "Enable SukiSU KPM"
|
||||||
default n
|
default n
|
||||||
help
|
help
|
||||||
This option enables the KernelSU KPM feature. If enabled, it will
|
Enabling this option will activate the KPM feature of SukiSU.
|
||||||
override the kernel version check and enable the hook functionality.
|
This option is suitable for scenarios where you need to force KPM to be enabled.
|
||||||
|
but it may affect system stability.
|
||||||
|
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ obj-$(CONFIG_KPM) += kpm/
|
|||||||
# .git is a text file while the module is imported by 'git submodule add'.
|
# .git is a text file while the module is imported by 'git submodule add'.
|
||||||
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
|
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)
|
$(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 HEAD)
|
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
|
# ksu_version: major * 10000 + git version + 606 for historical reasons
|
||||||
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
||||||
$(info -- KernelSU version: $(KSU_VERSION))
|
$(info -- KernelSU version: $(KSU_VERSION))
|
||||||
@@ -42,14 +42,32 @@ endif
|
|||||||
|
|
||||||
ifdef KSU_MANAGER_PACKAGE
|
ifdef KSU_MANAGER_PACKAGE
|
||||||
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
||||||
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
$(info -- SukiSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||||
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
$(info -- SukiSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||||
$(info -- Supported Unofficial Manager: ShirkNeko (GKI) (Non-GKI))
|
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
|
||||||
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
||||||
|
KERNEL_TYPE := Non-GKI
|
||||||
|
# Check for GKI 2.0 (5.10+ or 6.x+)
|
||||||
|
ifneq ($(shell test \( $(VERSION) -ge 5 -a $(PATCHLEVEL) -ge 10 \) -o $(VERSION) -ge 6; echo $$?),0)
|
||||||
|
# Check for GKI 1.0 (5.4)
|
||||||
|
ifeq ($(shell test $(VERSION)-$(PATCHLEVEL) = 5-4; echo $$?),0)
|
||||||
|
KERNEL_TYPE := GKI 1.0
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
KERNEL_TYPE := GKI 2.0
|
||||||
|
endif
|
||||||
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||||
|
$(info -- KERNEL_TYPE: $(KERNEL_TYPE))
|
||||||
|
|
||||||
|
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
|
||||||
|
ifeq ($(CONFIG_KPM),y)
|
||||||
|
$(info -- KPM is enabled)
|
||||||
|
else
|
||||||
|
$(info -- KPM is disabled)
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
||||||
|
|||||||
@@ -419,10 +419,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
|
|
||||||
pr_info("KPM: calling before arg2=%d\n", (int) arg2);
|
pr_info("KPM: calling before arg2=%d\n", (int) arg2);
|
||||||
|
|
||||||
res = sukisu_handle_kpm(arg2, arg3, arg4);
|
res = sukisu_handle_kpm(arg2, arg3, arg4, arg5);
|
||||||
copy_to_user(result, &res, sizeof(res));
|
|
||||||
|
|
||||||
pr_info("KPM: calling before arg2=%d res=%d\n", (int) arg2, (int) res);
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
obj-y += kpm.o
|
obj-y += kpm.o
|
||||||
obj-y += compact.o
|
obj-y += compact.o
|
||||||
|
obj-y += super_access.o
|
||||||
|
|
||||||
|
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
|
||||||
|
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function
|
||||||
@@ -26,37 +26,56 @@
|
|||||||
#include <linux/slab.h>
|
#include <linux/slab.h>
|
||||||
#include "kpm.h"
|
#include "kpm.h"
|
||||||
#include "compact.h"
|
#include "compact.h"
|
||||||
|
#include "../allowlist.h"
|
||||||
|
#include "../manager.h"
|
||||||
|
|
||||||
unsigned long sukisu_compact_find_symbol(const char* name);
|
unsigned long sukisu_compact_find_symbol(const char* name);
|
||||||
|
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
|
// 兼容函数 for KPM
|
||||||
|
|
||||||
const char* kpver = "0.10";
|
static
|
||||||
|
int sukisu_is_su_allow_uid(uid_t uid) {
|
||||||
|
return ksu_is_allow_uid(uid) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int sukisu_get_ap_mod_exclude(uid_t uid) {
|
||||||
|
// Not supported
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int sukisu_is_uid_should_umount(uid_t uid) {
|
||||||
|
return ksu_uid_should_umount(uid) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
int sukisu_is_current_uid_manager() {
|
||||||
|
return is_manager();
|
||||||
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
uid_t sukisu_get_manager_uid() {
|
||||||
|
return ksu_manager_uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================
|
||||||
|
|
||||||
struct CompactAddressSymbol {
|
struct CompactAddressSymbol {
|
||||||
const char* symbol_name;
|
const char* symbol_name;
|
||||||
void* addr;
|
void* addr;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CompactAliasSymbol {
|
static struct CompactAddressSymbol address_symbol [] = {
|
||||||
const char* symbol_name;
|
|
||||||
const char* compact_symbol_name;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CompactAddressSymbol address_symbol [] = {
|
|
||||||
{ "kallsyms_lookup_name", &kallsyms_lookup_name },
|
{ "kallsyms_lookup_name", &kallsyms_lookup_name },
|
||||||
{ "compact_find_symbol", &sukisu_compact_find_symbol },
|
{ "compact_find_symbol", &sukisu_compact_find_symbol },
|
||||||
{ "compat_copy_to_user", ©_to_user },
|
{ "is_run_in_sukisu_ultra", (void*)1 },
|
||||||
{ "compat_strncpy_from_user", &strncpy_from_user },
|
{ "is_su_allow_uid", &sukisu_is_su_allow_uid },
|
||||||
{ "kpver", &kpver },
|
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
|
||||||
{ "is_run_in_sukisu_ultra", (void*)1 }
|
{ "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 }
|
||||||
struct CompactAliasSymbol alias_symbol[] = {
|
|
||||||
{"kf_strncat", "strncat"},
|
|
||||||
{"kf_strlen", "strlen" },
|
|
||||||
{"kf_strcpy", "strcpy"},
|
|
||||||
{"compat_copy_to_user", "__arch_copy_to_user"}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned long sukisu_compact_find_symbol(const char* name) {
|
unsigned long sukisu_compact_find_symbol(const char* name) {
|
||||||
@@ -71,30 +90,13 @@ unsigned long sukisu_compact_find_symbol(const char* name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 如果符号名以 "kf__" 开头,尝试解析去掉前缀的部分 */
|
|
||||||
if (strncmp(name, "kf__", 4) == 0) {
|
|
||||||
const char *real_name = name + 4; // 去掉 "kf__"
|
|
||||||
addr = (unsigned long)kallsyms_lookup_name(real_name);
|
|
||||||
if (addr) {
|
|
||||||
return addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 通过内核来查
|
// 通过内核来查
|
||||||
addr = kallsyms_lookup_name(name);
|
addr = kallsyms_lookup_name(name);
|
||||||
if(addr) {
|
if(addr) {
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查不到就查查兼容的符号
|
|
||||||
for(i = 0; i < (sizeof(alias_symbol) / sizeof(struct CompactAliasSymbol)); i++) {
|
|
||||||
struct CompactAliasSymbol* symbol = &alias_symbol[i];
|
|
||||||
if(strcmp(name, symbol->symbol_name) == 0) {
|
|
||||||
addr = kallsyms_lookup_name(symbol->compact_symbol_name);
|
|
||||||
if(addr)
|
|
||||||
return addr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EXPORT_SYMBOL(sukisu_compact_find_symbol);
|
||||||
|
|||||||
1340
kernel/kpm/kpm.c
1340
kernel/kpm/kpm.c
File diff suppressed because it is too large
Load Diff
@@ -1,44 +1,44 @@
|
|||||||
#ifndef ___SUKISU_KPM_H
|
#ifndef ___SUKISU_KPM_H
|
||||||
#define ___SUKISU_KPM_H
|
#define ___SUKISU_KPM_H
|
||||||
|
|
||||||
int sukisu_handle_kpm(unsigned long arg3, unsigned long arg4, unsigned long arg5);
|
int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
|
||||||
int sukisu_is_kpm_control_code(unsigned long arg2);
|
int sukisu_is_kpm_control_code(unsigned long arg2);
|
||||||
|
|
||||||
// KPM控制代码
|
// KPM控制代码
|
||||||
#define CMD_KPM_CONTROL 28
|
#define CMD_KPM_CONTROL 28
|
||||||
#define CMD_KPM_CONTROL_MAX 34
|
#define CMD_KPM_CONTROL_MAX 35
|
||||||
|
|
||||||
// 控制代码
|
// 控制代码
|
||||||
|
|
||||||
// prctl(xxx, xxx, 1, "PATH", "ARGS")
|
// prctl(xxx, 28, "PATH", "ARGS")
|
||||||
// success return 0, error return -N
|
// success return 0, error return -N
|
||||||
#define SUKISU_KPM_LOAD 28
|
#define SUKISU_KPM_LOAD 28
|
||||||
|
|
||||||
// prctl(xxx, xxx, 2, "NAME")
|
// prctl(xxx, 29, "NAME")
|
||||||
// success return 0, error return -N
|
// success return 0, error return -N
|
||||||
#define SUKISU_KPM_UNLOAD 29
|
#define SUKISU_KPM_UNLOAD 29
|
||||||
|
|
||||||
// num = prctl(xxx, xxx, 3)
|
// num = prctl(xxx, 30)
|
||||||
// error return -N
|
// error return -N
|
||||||
// success return +num or 0
|
// success return +num or 0
|
||||||
#define SUKISU_KPM_NUM 30
|
#define SUKISU_KPM_NUM 30
|
||||||
|
|
||||||
// prctl(xxx, xxx, 4, Buffer, BufferSize)
|
// prctl(xxx, 31, Buffer, BufferSize)
|
||||||
// success return +out, error return -N
|
// success return +out, error return -N
|
||||||
#define SUKISU_KPM_LIST 31
|
#define SUKISU_KPM_LIST 31
|
||||||
|
|
||||||
// prctl(xxx, xxx, 5, "NAME", Buffer[256])
|
// prctl(xxx, 32, "NAME", Buffer[256])
|
||||||
// success return +out, error return -N
|
// success return +out, error return -N
|
||||||
#define SUKISU_KPM_INFO 32
|
#define SUKISU_KPM_INFO 32
|
||||||
|
|
||||||
// prctl(xxx, xxx, 6, "NAME", "ARGS")
|
// prctl(xxx, 33, "NAME", "ARGS")
|
||||||
// success return KPM's result value
|
// success return KPM's result value
|
||||||
// error return -N
|
// error return -N
|
||||||
#define SUKISU_KPM_CONTROL 33
|
#define SUKISU_KPM_CONTROL 33
|
||||||
|
|
||||||
// prctl(xxx, xxx, 7)
|
// prctl(xxx, 34, buffer, bufferSize)
|
||||||
// success will printf to stdout and return 0
|
// success return KPM's result value
|
||||||
// error will return -1
|
// error return -N
|
||||||
#define SUKISU_KPM_PRINT 34
|
#define SUKISU_KPM_VERSION 34
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
#include <linux/export.h>
|
||||||
|
#include <linux/module.h>
|
||||||
|
#include <linux/kernel.h>
|
||||||
|
#include <linux/fs.h>
|
||||||
|
#include <linux/kernfs.h>
|
||||||
|
#include <linux/file.h>
|
||||||
|
#include <linux/slab.h>
|
||||||
|
#include <linux/vmalloc.h>
|
||||||
|
#include <linux/uaccess.h>
|
||||||
|
#include <linux/elf.h>
|
||||||
|
#include <linux/kallsyms.h>
|
||||||
|
#include <linux/version.h>
|
||||||
|
#include <linux/list.h>
|
||||||
|
#include <linux/spinlock.h>
|
||||||
|
#include <linux/rcupdate.h>
|
||||||
|
#include <asm/elf.h> /* 包含 ARM64 重定位类型定义 */
|
||||||
|
#include <linux/vmalloc.h>
|
||||||
|
#include <linux/mm.h>
|
||||||
|
#include <linux/string.h>
|
||||||
|
#include <asm/cacheflush.h>
|
||||||
|
#include <linux/module.h>
|
||||||
|
#include <linux/vmalloc.h>
|
||||||
|
#include <linux/set_memory.h>
|
||||||
|
#include <linux/version.h>
|
||||||
|
#include <linux/export.h>
|
||||||
|
#include <linux/slab.h>
|
||||||
|
#include "kpm.h"
|
||||||
|
#include "compact.h"
|
||||||
|
#include <linux/types.h>
|
||||||
|
#include <linux/stddef.h>
|
||||||
|
|
||||||
|
// 结构体成员元数据
|
||||||
|
struct DynamicStructMember {
|
||||||
|
const char* name;
|
||||||
|
size_t size;
|
||||||
|
size_t offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 结构体元数据(包含总大小)
|
||||||
|
struct DynamicStructInfo {
|
||||||
|
const char* name;
|
||||||
|
size_t count;
|
||||||
|
size_t total_size;
|
||||||
|
struct DynamicStructMember* members;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义结构体元数据的宏(直接使用 struct 名称)
|
||||||
|
#define DYNAMIC_STRUCT_BEGIN(struct_name) \
|
||||||
|
static struct DynamicStructMember struct_name##_members[] = {
|
||||||
|
|
||||||
|
#define DEFINE_MEMBER(struct_name, member) \
|
||||||
|
{ \
|
||||||
|
.name = #member, \
|
||||||
|
.size = sizeof(((struct struct_name*)0)->member), \
|
||||||
|
.offset = offsetof(struct struct_name, member) \
|
||||||
|
},
|
||||||
|
|
||||||
|
#define DYNAMIC_STRUCT_END(struct_name) \
|
||||||
|
}; \
|
||||||
|
static struct DynamicStructInfo struct_name##_info = { \
|
||||||
|
.name = #struct_name, \
|
||||||
|
.count = sizeof(struct_name##_members) / sizeof(struct DynamicStructMember), \
|
||||||
|
.total_size = sizeof(struct struct_name), \
|
||||||
|
.members = struct_name##_members \
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==================================================================================
|
||||||
|
|
||||||
|
#include <linux/version.h>
|
||||||
|
|
||||||
|
#define KERNEL_VERSION_6_1 KERNEL_VERSION(6, 1, 0)
|
||||||
|
#define KERNEL_VERSION_5_15 KERNEL_VERSION(5, 15, 0)
|
||||||
|
|
||||||
|
#include <../fs/mount.h>
|
||||||
|
#include <linux/mount.h>
|
||||||
|
|
||||||
|
// 定义元数据
|
||||||
|
DYNAMIC_STRUCT_BEGIN(mount)
|
||||||
|
DEFINE_MEMBER(mount, mnt_parent)
|
||||||
|
DEFINE_MEMBER(mount, mnt)
|
||||||
|
DEFINE_MEMBER(mount, mnt_id)
|
||||||
|
DEFINE_MEMBER(mount, mnt_group_id)
|
||||||
|
DEFINE_MEMBER(mount, mnt_expiry_mark)
|
||||||
|
DEFINE_MEMBER(mount, mnt_master)
|
||||||
|
DEFINE_MEMBER(mount, mnt_devname)
|
||||||
|
DYNAMIC_STRUCT_END(mount)
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(vfsmount)
|
||||||
|
DEFINE_MEMBER(vfsmount, mnt_root)
|
||||||
|
DEFINE_MEMBER(vfsmount, mnt_sb)
|
||||||
|
DEFINE_MEMBER(vfsmount, mnt_flags)
|
||||||
|
DYNAMIC_STRUCT_END(vfsmount)
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(mnt_namespace)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, ns)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, root)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, seq)
|
||||||
|
DEFINE_MEMBER(mnt_namespace, mounts)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION_5_15
|
||||||
|
DEFINE_MEMBER(mnt_namespace, count)
|
||||||
|
#endif
|
||||||
|
DYNAMIC_STRUCT_END(mnt_namespace)
|
||||||
|
|
||||||
|
#include <linux/kprobes.h>
|
||||||
|
|
||||||
|
#ifdef CONFIG_KPROBES
|
||||||
|
DYNAMIC_STRUCT_BEGIN(kprobe)
|
||||||
|
DEFINE_MEMBER(kprobe, addr)
|
||||||
|
DEFINE_MEMBER(kprobe, symbol_name)
|
||||||
|
DEFINE_MEMBER(kprobe, offset)
|
||||||
|
DEFINE_MEMBER(kprobe, pre_handler)
|
||||||
|
DEFINE_MEMBER(kprobe, post_handler)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION_5_15
|
||||||
|
DEFINE_MEMBER(kprobe, fault_handler)
|
||||||
|
#endif
|
||||||
|
DEFINE_MEMBER(kprobe, flags)
|
||||||
|
DYNAMIC_STRUCT_END(kprobe)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <linux/mm.h>
|
||||||
|
#include <linux/mm_types.h>
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(vm_area_struct)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_start)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_end)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_flags)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,anon_vma)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_pgoff)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_file)
|
||||||
|
DEFINE_MEMBER(vm_area_struct,vm_private_data)
|
||||||
|
#ifdef CONFIG_ANON_VMA_NAME
|
||||||
|
DEFINE_MEMBER(vm_area_struct, anon_name)
|
||||||
|
#endif
|
||||||
|
DEFINE_MEMBER(vm_area_struct, vm_ops)
|
||||||
|
DYNAMIC_STRUCT_END(vm_area_struct)
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(vm_operations_struct)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, open)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, close)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, name)
|
||||||
|
DEFINE_MEMBER(vm_operations_struct, access)
|
||||||
|
DYNAMIC_STRUCT_END(vm_operations_struct)
|
||||||
|
|
||||||
|
#include <linux/netlink.h>
|
||||||
|
|
||||||
|
DYNAMIC_STRUCT_BEGIN(netlink_kernel_cfg)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, groups)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, flags)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, input)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, cb_mutex)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, bind)
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, unbind)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION_6_1
|
||||||
|
DEFINE_MEMBER(netlink_kernel_cfg, compare)
|
||||||
|
#endif
|
||||||
|
DYNAMIC_STRUCT_END(netlink_kernel_cfg)
|
||||||
|
|
||||||
|
|
||||||
|
#include <linux/sched.h>
|
||||||
|
DYNAMIC_STRUCT_BEGIN(task_struct)
|
||||||
|
DEFINE_MEMBER(task_struct, pid)
|
||||||
|
DEFINE_MEMBER(task_struct, tgid)
|
||||||
|
DEFINE_MEMBER(task_struct, cred)
|
||||||
|
DEFINE_MEMBER(task_struct, real_cred)
|
||||||
|
DEFINE_MEMBER(task_struct, comm)
|
||||||
|
DEFINE_MEMBER(task_struct, parent)
|
||||||
|
DEFINE_MEMBER(task_struct, group_leader)
|
||||||
|
DEFINE_MEMBER(task_struct, mm)
|
||||||
|
DEFINE_MEMBER(task_struct, active_mm)
|
||||||
|
DEFINE_MEMBER(task_struct, thread_pid)
|
||||||
|
DEFINE_MEMBER(task_struct, files)
|
||||||
|
DEFINE_MEMBER(task_struct, seccomp)
|
||||||
|
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||||
|
DEFINE_MEMBER(task_struct, thread_info)
|
||||||
|
#endif
|
||||||
|
#ifdef CONFIG_CGROUPS
|
||||||
|
DEFINE_MEMBER(task_struct, cgroups)
|
||||||
|
#endif
|
||||||
|
#ifdef CONFIG_SECURITY
|
||||||
|
DEFINE_MEMBER(task_struct, security)
|
||||||
|
#endif
|
||||||
|
DEFINE_MEMBER(task_struct, thread)
|
||||||
|
DYNAMIC_STRUCT_END(task_struct)
|
||||||
|
|
||||||
|
// =====================================================================================================================
|
||||||
|
|
||||||
|
#define STRUCT_INFO(name) &(name##_info)
|
||||||
|
|
||||||
|
static
|
||||||
|
struct DynamicStructInfo* dynamic_struct_infos[] = {
|
||||||
|
STRUCT_INFO(mount),
|
||||||
|
STRUCT_INFO(vfsmount),
|
||||||
|
STRUCT_INFO(mnt_namespace),
|
||||||
|
#ifdef CONFIG_KPROBES
|
||||||
|
STRUCT_INFO(kprobe),
|
||||||
|
#endif
|
||||||
|
STRUCT_INFO(vm_area_struct),
|
||||||
|
STRUCT_INFO(vm_operations_struct),
|
||||||
|
STRUCT_INFO(netlink_kernel_cfg),
|
||||||
|
STRUCT_INFO(task_struct)
|
||||||
|
};
|
||||||
|
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
int sukisu_super_find_struct(
|
||||||
|
const char* struct_name,
|
||||||
|
size_t* out_size,
|
||||||
|
int* out_members
|
||||||
|
) {
|
||||||
|
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
|
||||||
|
struct DynamicStructInfo* info = dynamic_struct_infos[i];
|
||||||
|
if(strcmp(struct_name, info->name) == 0) {
|
||||||
|
if(out_size)
|
||||||
|
*out_size = info->total_size;
|
||||||
|
if(out_members)
|
||||||
|
*out_members = info->count;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EXPORT_SYMBOL(sukisu_super_find_struct);
|
||||||
|
|
||||||
|
// Dynamic access struct
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
// return -2 if member not defined
|
||||||
|
int sukisu_super_access (
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
size_t* out_offset,
|
||||||
|
size_t* out_size
|
||||||
|
) {
|
||||||
|
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
|
||||||
|
struct DynamicStructInfo* info = dynamic_struct_infos[i];
|
||||||
|
if(strcmp(struct_name, info->name) == 0) {
|
||||||
|
for (size_t i1 = 0; i1 < info->count; i1++) {
|
||||||
|
if (strcmp(info->members[i1].name, member_name) == 0) {
|
||||||
|
if(out_offset)
|
||||||
|
*out_offset = info->members[i].offset;
|
||||||
|
if(out_size)
|
||||||
|
*out_size = info->members[i].size;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EXPORT_SYMBOL(sukisu_super_access);
|
||||||
|
|
||||||
|
// 动态 container_of 宏
|
||||||
|
#define DYNAMIC_CONTAINER_OF(offset, member_ptr) ({ \
|
||||||
|
(offset != (size_t)-1) ? (void*)((char*)(member_ptr) - offset) : NULL; \
|
||||||
|
})
|
||||||
|
|
||||||
|
// Dynamic container_of
|
||||||
|
// return 0 if success
|
||||||
|
// return -1 if current struct not defined
|
||||||
|
// return -2 if target member not defined
|
||||||
|
int sukisu_super_container_of(
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
void* ptr,
|
||||||
|
void** out_ptr
|
||||||
|
) {
|
||||||
|
if(ptr == NULL) {
|
||||||
|
return -3;
|
||||||
|
}
|
||||||
|
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
|
||||||
|
struct DynamicStructInfo* info = dynamic_struct_infos[i];
|
||||||
|
if(strcmp(struct_name, info->name) == 0) {
|
||||||
|
for (size_t i1 = 0; i1 < info->count; i1++) {
|
||||||
|
if (strcmp(info->members[i1].name, member_name) == 0) {
|
||||||
|
*out_ptr = (void*) DYNAMIC_CONTAINER_OF(info->members[i1].offset, ptr);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
EXPORT_SYMBOL(sukisu_super_container_of);
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#ifndef __SUKISU_SUPER_ACCESS_H
|
||||||
|
#define __SUKISU_SUPER_ACCESS_H
|
||||||
|
|
||||||
|
#include <linux/types.h>
|
||||||
|
#include <linux/stddef.h>
|
||||||
|
#include "kpm.h"
|
||||||
|
#include "compact.h"
|
||||||
|
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
int sukisu_super_find_struct(
|
||||||
|
const char* struct_name,
|
||||||
|
size_t* out_size,
|
||||||
|
int* out_members
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dynamic access struct
|
||||||
|
// return 0 if successful
|
||||||
|
// return -1 if struct not defined
|
||||||
|
// return -2 if member not defined
|
||||||
|
int sukisu_super_access (
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
size_t* out_offset,
|
||||||
|
size_t* out_size
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dynamic container_of
|
||||||
|
// return 0 if success
|
||||||
|
// return -1 if current struct not defined
|
||||||
|
// return -2 if target member not defined
|
||||||
|
int sukisu_super_container_of(
|
||||||
|
const char* struct_name,
|
||||||
|
const char* member_name,
|
||||||
|
void* ptr,
|
||||||
|
void** out_ptr
|
||||||
|
);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
|
|||||||
// Detect whether it is on or not
|
// Detect whether it is on or not
|
||||||
static bool is_boot_phase = true;
|
static bool is_boot_phase = true;
|
||||||
|
|
||||||
|
#ifdef CONFIG_COMPAT
|
||||||
|
bool ksu_is_compat __read_mostly = false;
|
||||||
|
#endif
|
||||||
|
|
||||||
void on_post_fs_data(void)
|
void on_post_fs_data(void)
|
||||||
{
|
{
|
||||||
static bool done = false;
|
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))
|
if (get_user(compat, argv.ptr.compat + nr))
|
||||||
return ERR_PTR(-EFAULT);
|
return ERR_PTR(-EFAULT);
|
||||||
|
|
||||||
|
ksu_is_compat = true;
|
||||||
return compat_ptr(compat);
|
return compat_ptr(compat);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
|
|||||||
#define CMD_TYPE_CHANGE 8
|
#define CMD_TYPE_CHANGE 8
|
||||||
#define CMD_GENFSCON 9
|
#define CMD_GENFSCON 9
|
||||||
|
|
||||||
|
#ifdef CONFIG_64BIT
|
||||||
struct sepol_data {
|
struct sepol_data {
|
||||||
u32 cmd;
|
u32 cmd;
|
||||||
u32 subcmd;
|
u32 subcmd;
|
||||||
char __user *sepol1;
|
u64 field_sepol1;
|
||||||
char __user *sepol2;
|
u64 field_sepol2;
|
||||||
char __user *sepol3;
|
u64 field_sepol3;
|
||||||
char __user *sepol4;
|
u64 field_sepol4;
|
||||||
char __user *sepol5;
|
u64 field_sepol5;
|
||||||
char __user *sepol6;
|
u64 field_sepol6;
|
||||||
char __user *sepol7;
|
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,
|
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
|
||||||
char **object)
|
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");
|
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;
|
struct sepol_data data;
|
||||||
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||||
pr_err("sepol: copy sepol_data failed.\n");
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
sepol1 = data.field_sepol1;
|
||||||
u32 cmd = data.cmd;
|
sepol2 = data.field_sepol2;
|
||||||
u32 subcmd = data.subcmd;
|
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();
|
rcu_read_lock();
|
||||||
|
|
||||||
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char perm_buf[MAX_SEPOL_LEN];
|
char perm_buf[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
char *s, *t, *c, *p;
|
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");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
|
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy perm failed.\n");
|
pr_err("sepol: copy perm failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char perm_set[MAX_SEPOL_LEN];
|
char perm_set[MAX_SEPOL_LEN];
|
||||||
|
|
||||||
char *s, *t, *c;
|
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");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(operation, data.sepol4,
|
if (strncpy_from_user(operation, sepol4,
|
||||||
sizeof(operation)) < 0) {
|
sizeof(operation)) < 0) {
|
||||||
pr_err("sepol: copy operation failed.\n");
|
pr_err("sepol: copy operation failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
|
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy perm_set failed.\n");
|
pr_err("sepol: copy perm_set failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
} else if (cmd == CMD_TYPE_STATE) {
|
} else if (cmd == CMD_TYPE_STATE) {
|
||||||
char src[MAX_SEPOL_LEN];
|
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");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char type[MAX_SEPOL_LEN];
|
char type[MAX_SEPOL_LEN];
|
||||||
char attr[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");
|
pr_err("sepol: copy type failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy attr failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
} else if (cmd == CMD_ATTR) {
|
} else if (cmd == CMD_ATTR) {
|
||||||
char attr[MAX_SEPOL_LEN];
|
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");
|
pr_err("sepol: copy attr failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char default_type[MAX_SEPOL_LEN];
|
char default_type[MAX_SEPOL_LEN];
|
||||||
char object[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");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(default_type, data.sepol4,
|
if (strncpy_from_user(default_type, sepol4,
|
||||||
sizeof(default_type)) < 0) {
|
sizeof(default_type)) < 0) {
|
||||||
pr_err("sepol: copy default_type failed.\n");
|
pr_err("sepol: copy default_type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
char *real_object;
|
char *real_object;
|
||||||
if (data.sepol5 == NULL) {
|
if (sepol5 == NULL) {
|
||||||
real_object = NULL;
|
real_object = NULL;
|
||||||
} else {
|
} else {
|
||||||
if (strncpy_from_user(object, data.sepol5,
|
if (strncpy_from_user(object, sepol5,
|
||||||
sizeof(object)) < 0) {
|
sizeof(object)) < 0) {
|
||||||
pr_err("sepol: copy object failed.\n");
|
pr_err("sepol: copy object failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char cls[MAX_SEPOL_LEN];
|
char cls[MAX_SEPOL_LEN];
|
||||||
char default_type[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");
|
pr_err("sepol: copy src failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy tgt failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy cls failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(default_type, data.sepol4,
|
if (strncpy_from_user(default_type, sepol4,
|
||||||
sizeof(default_type)) < 0) {
|
sizeof(default_type)) < 0) {
|
||||||
pr_err("sepol: copy default_type failed.\n");
|
pr_err("sepol: copy default_type failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
char name[MAX_SEPOL_LEN];
|
char name[MAX_SEPOL_LEN];
|
||||||
char path[MAX_SEPOL_LEN];
|
char path[MAX_SEPOL_LEN];
|
||||||
char context[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");
|
pr_err("sepol: copy name failed.\n");
|
||||||
goto exit;
|
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");
|
pr_err("sepol: copy path failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
}
|
}
|
||||||
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
|
if (strncpy_from_user(context, sepol3, sizeof(context)) <
|
||||||
0) {
|
0) {
|
||||||
pr_err("sepol: copy context failed.\n");
|
pr_err("sepol: copy context failed.\n");
|
||||||
goto exit;
|
goto exit;
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
|
|
||||||
extern void escape_to_root();
|
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)
|
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
|
/* 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;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!ksu_is_allow_uid(current_uid().val)) {
|
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||||
return 0;
|
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 sh[] = SH_PATH;
|
||||||
const char su[] = SU_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)) {
|
if (!ksu_is_allow_uid(current_uid().val)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
|
|||||||
const char sh[] = KSUD_PATH;
|
const char sh[] = KSUD_PATH;
|
||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_ptr))
|
if (unlikely(!filename_ptr))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
|||||||
const char su[] = SU_PATH;
|
const char su[] = SU_PATH;
|
||||||
char path[sizeof(su) + 1];
|
char path[sizeof(su) + 1];
|
||||||
|
|
||||||
|
#ifndef CONFIG_KPROBES
|
||||||
|
if (!ksu_sucompat_non_kp){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_user))
|
if (unlikely(!filename_user))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
|
|||||||
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
|
||||||
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
|
||||||
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
|
|||||||
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
|
||||||
destroy_kprobe(&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
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,12 +358,14 @@ void track_throne()
|
|||||||
if (ksu_is_manager_uid_valid()) {
|
if (ksu_is_manager_uid_valid()) {
|
||||||
pr_info("manager is uninstalled, invalidate it!\n");
|
pr_info("manager is uninstalled, invalidate it!\n");
|
||||||
ksu_invalidate_manager_uid();
|
ksu_invalidate_manager_uid();
|
||||||
|
goto prune;
|
||||||
}
|
}
|
||||||
pr_info("Searching manager...\n");
|
pr_info("Searching manager...\n");
|
||||||
search_manager("/data/app", 2, &uid_list);
|
search_manager("/data/app", 2, &uid_list);
|
||||||
pr_info("Search manager finished\n");
|
pr_info("Search manager finished\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prune:
|
||||||
// then prune the allowlist
|
// then prune the allowlist
|
||||||
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
ksu_prune_allowlist(is_uid_exist, &uid_list);
|
||||||
out:
|
out:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@file:Suppress("UnstableApiUsage")
|
@file:Suppress("UnstableApiUsage")
|
||||||
|
|
||||||
|
import com.android.build.api.dsl.ApkSigningConfig
|
||||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||||
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
import com.android.build.gradle.tasks.PackageAndroidArtifact
|
||||||
|
|
||||||
@@ -24,8 +25,18 @@ apksign {
|
|||||||
keyPasswordProperty = "KEY_PASSWORD"
|
keyPasswordProperty = "KEY_PASSWORD"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "shirkneko.zako.sukisu"
|
|
||||||
|
/**signingConfigs {
|
||||||
|
create("Debug") {
|
||||||
|
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||||
|
storePassword = ""
|
||||||
|
keyAlias = ""
|
||||||
|
keyPassword = ""
|
||||||
|
}
|
||||||
|
}**/
|
||||||
|
namespace = "com.sukisu.ultra"
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
@@ -33,6 +44,9 @@ android {
|
|||||||
isShrinkResources = true
|
isShrinkResources = true
|
||||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||||
}
|
}
|
||||||
|
/**debug {
|
||||||
|
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
|
||||||
|
}**/
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -140,4 +154,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.com.github.topjohnwu.libsu.core)
|
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 { *; }
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
@@ -37,6 +39,13 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:theme="@style/Theme.KernelSU.WebUI" />
|
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
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// IKsuInterface.aidl
|
package com.sukisu.zako;
|
||||||
package shirkneko.zako.sukisu;
|
|
||||||
|
|
||||||
import android.content.pm.PackageInfo;
|
import android.content.pm.PackageInfo;
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
import rikka.parcelablelist.ParcelableListSlice;
|
||||||
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
BIN
manager/app/src/main/assets/5_15+-mkbootfs
Normal file
Binary file not shown.
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
||||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
||||||
auto result = become_manager(cpkg);
|
auto result = become_manager(cpkg);
|
||||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
env->ReleaseStringUTFChars(pkg, cpkg);
|
||||||
@@ -21,13 +21,13 @@ Java_shirkneko_zako_sukisu_Natives_becomeManager(JNIEnv *env, jobject, jstring p
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jint JNICALL
|
JNIEXPORT jint JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_getVersion(JNIEnv *env, jobject) {
|
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
|
||||||
return get_version();
|
return get_version();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jintArray JNICALL
|
JNIEXPORT jintArray JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_getAllowList(JNIEnv *env, jobject) {
|
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
|
||||||
int uids[1024];
|
int uids[1024];
|
||||||
int size = 0;
|
int size = 0;
|
||||||
bool result = get_allow_list(uids, &size);
|
bool result = get_allow_list(uids, &size);
|
||||||
@@ -42,13 +42,13 @@ Java_shirkneko_zako_sukisu_Natives_getAllowList(JNIEnv *env, jobject) {
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
||||||
return is_safe_mode();
|
return is_safe_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
||||||
return is_lkm_mode();
|
return is_lkm_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jobject JNICALL
|
JNIEXPORT jobject JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
||||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -129,7 +129,7 @@ Java_shirkneko_zako_sukisu_Natives_getAppProfile(JNIEnv *env, jobject, jstring p
|
|||||||
|
|
||||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||||
|
|
||||||
auto cls = env->FindClass("shirkneko/zako/sukisu/Natives$Profile");
|
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||||
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
||||||
auto obj = env->NewObject(cls, constructor);
|
auto obj = env->NewObject(cls, constructor);
|
||||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||||
@@ -207,8 +207,8 @@ Java_shirkneko_zako_sukisu_Natives_getAppProfile(JNIEnv *env, jobject, jstring p
|
|||||||
|
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
||||||
auto cls = env->FindClass("shirkneko/zako/sukisu/Natives$Profile");
|
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
||||||
|
|
||||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
||||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
||||||
@@ -293,16 +293,16 @@ Java_shirkneko_zako_sukisu_Natives_setAppProfile(JNIEnv *env, jobject clazz, job
|
|||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
||||||
return uid_should_umount(uid);
|
return uid_should_umount(uid);
|
||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
||||||
return is_su_enabled();
|
return is_su_enabled();
|
||||||
}
|
}
|
||||||
extern "C"
|
extern "C"
|
||||||
JNIEXPORT jboolean JNICALL
|
JNIEXPORT jboolean JNICALL
|
||||||
Java_shirkneko_zako_sukisu_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
||||||
return set_su_enabled(enabled);
|
return set_su_enabled(enabled);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.sukisu.ultra
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
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
|
||||||
|
|
||||||
|
lateinit var ksuApp: KernelSUApplication
|
||||||
|
|
||||||
|
class KernelSUApplication : Application() {
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Platform.setHiddenApiExemptions()
|
||||||
|
|
||||||
|
val context = this
|
||||||
|
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||||
|
Coil.setImageLoader(
|
||||||
|
ImageLoader.Builder(context)
|
||||||
|
.components {
|
||||||
|
add(AppIconKeyer())
|
||||||
|
add(AppIconFetcher.Factory(iconSize, false, context))
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
|
||||||
|
val webroot = File(dataDir, "webroot")
|
||||||
|
if (!webroot.exists()) {
|
||||||
|
webroot.mkdir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu
|
package com.sukisu.ultra
|
||||||
|
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu
|
package com.sukisu.ultra
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
446
manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt
Normal file
@@ -0,0 +1,446 @@
|
|||||||
|
package com.sukisu.ultra.flash
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.CheckCircle
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.utils.AssetsUtil
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class FlashState(
|
||||||
|
val isFlashing: Boolean = false,
|
||||||
|
val isCompleted: Boolean = false,
|
||||||
|
val progress: Float = 0f,
|
||||||
|
val currentStep: String = "",
|
||||||
|
val logs: List<String> = emptyList(),
|
||||||
|
val error: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
class HorizonKernelState {
|
||||||
|
private val _state = MutableStateFlow(FlashState())
|
||||||
|
val state: StateFlow<FlashState> = _state.asStateFlow()
|
||||||
|
|
||||||
|
fun updateProgress(progress: Float) {
|
||||||
|
_state.update { it.copy(progress = progress) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateStep(step: String) {
|
||||||
|
_state.update { it.copy(currentStep = step) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLog(log: String) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(logs = it.logs + log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setError(error: String) {
|
||||||
|
_state.update { it.copy(error = error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startFlashing() {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
isFlashing = true,
|
||||||
|
isCompleted = false,
|
||||||
|
progress = 0f,
|
||||||
|
currentStep = "under preparation...",
|
||||||
|
logs = emptyList(),
|
||||||
|
error = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun completeFlashing() {
|
||||||
|
_state.update { it.copy(isCompleted = true, progress = 1f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
_state.value = FlashState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizonKernelWorker(
|
||||||
|
private val context: Context,
|
||||||
|
private val state: HorizonKernelState,
|
||||||
|
private val slot: String? = null
|
||||||
|
) : Thread() {
|
||||||
|
var uri: Uri? = null
|
||||||
|
private lateinit var filePath: String
|
||||||
|
private lateinit var binaryPath: String
|
||||||
|
|
||||||
|
private var onFlashComplete: (() -> Unit)? = null
|
||||||
|
private var originalSlot: String? = null
|
||||||
|
|
||||||
|
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
||||||
|
onFlashComplete = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
state.startFlashing()
|
||||||
|
state.updateStep(context.getString(R.string.horizon_preparing))
|
||||||
|
|
||||||
|
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
||||||
|
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
||||||
|
|
||||||
|
try {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_cleaning_files))
|
||||||
|
state.updateProgress(0.1f)
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
if (!rootAvailable()) {
|
||||||
|
state.setError(context.getString(R.string.root_required))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_copying_files))
|
||||||
|
state.updateProgress(0.2f)
|
||||||
|
copy()
|
||||||
|
|
||||||
|
if (!File(filePath).exists()) {
|
||||||
|
state.setError(context.getString(R.string.horizon_copy_failed))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_extracting_tool))
|
||||||
|
state.updateProgress(0.4f)
|
||||||
|
getBinary()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_patching_script))
|
||||||
|
state.updateProgress(0.6f)
|
||||||
|
patch()
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_flashing))
|
||||||
|
state.updateProgress(0.7f)
|
||||||
|
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
|
|
||||||
|
if (isAbDevice && slot != null) {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_getting_original_slot))
|
||||||
|
state.updateProgress(0.72f)
|
||||||
|
originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_setting_target_slot))
|
||||||
|
state.updateProgress(0.74f)
|
||||||
|
runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot")
|
||||||
|
}
|
||||||
|
|
||||||
|
flash()
|
||||||
|
|
||||||
|
if (isAbDevice && !originalSlot.isNullOrEmpty()) {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||||
|
state.updateProgress(0.8f)
|
||||||
|
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.updateStep(context.getString(R.string.horizon_flash_complete_status))
|
||||||
|
state.completeFlashing()
|
||||||
|
|
||||||
|
(context as? Activity)?.runOnUiThread {
|
||||||
|
onFlashComplete?.invoke()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
state.setError(e.message ?: context.getString(R.string.horizon_unknown_error))
|
||||||
|
|
||||||
|
if (isAbDevice() && !originalSlot.isNullOrEmpty()) {
|
||||||
|
state.updateStep(context.getString(R.string.horizon_restoring_original_slot))
|
||||||
|
state.updateProgress(0.8f)
|
||||||
|
runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查设备是否为AB分区设备
|
||||||
|
private fun isAbDevice(): Boolean {
|
||||||
|
val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: ""
|
||||||
|
if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix")
|
||||||
|
return !slotSuffix.isNullOrEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanup() {
|
||||||
|
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copy() {
|
||||||
|
uri?.let { safeUri ->
|
||||||
|
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
||||||
|
FileOutputStream(File(filePath)).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getBinary() {
|
||||||
|
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
||||||
|
if (!File(binaryPath).exists()) {
|
||||||
|
throw IOException("Failed to extract update-binary")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun patch() {
|
||||||
|
val kernelVersion = runCommandGetOutput(true, "cat /proc/version")
|
||||||
|
val versionRegex = """\d+\.\d+\.\d+""".toRegex()
|
||||||
|
val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: ""
|
||||||
|
val toolName = if (version.isNotEmpty()) {
|
||||||
|
val parts = version.split('.')
|
||||||
|
if (parts.size >= 2) {
|
||||||
|
val major = parts[0].toIntOrNull() ?: 0
|
||||||
|
val minor = parts[1].toIntOrNull() ?: 0
|
||||||
|
if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+"
|
||||||
|
} else {
|
||||||
|
"5_15+"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"5_15+"
|
||||||
|
}
|
||||||
|
val toolPath = "${context.filesDir.absolutePath}/mkbootfs"
|
||||||
|
AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath)
|
||||||
|
state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}")
|
||||||
|
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun flash() {
|
||||||
|
val process = ProcessBuilder("su")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
||||||
|
|
||||||
|
// 写入槽位信息到临时文件
|
||||||
|
slot?.let { selectedSlot ->
|
||||||
|
writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建刷写命令
|
||||||
|
val flashCommand = buildString {
|
||||||
|
append("sh $binaryPath 3 1 \"$filePath\"")
|
||||||
|
if (slot != null) {
|
||||||
|
append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"")
|
||||||
|
}
|
||||||
|
append(" && touch ${context.filesDir.absolutePath}/done\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.write(flashCommand)
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.lineSequence().forEach { line ->
|
||||||
|
if (line.startsWith("ui_print")) {
|
||||||
|
val logMessage = line.removePrefix("ui_print").trim()
|
||||||
|
state.addLog(logMessage)
|
||||||
|
|
||||||
|
when {
|
||||||
|
logMessage.contains("extracting", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.75f)
|
||||||
|
}
|
||||||
|
logMessage.contains("installing", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.85f)
|
||||||
|
}
|
||||||
|
logMessage.contains("complete", ignoreCase = true) -> {
|
||||||
|
state.updateProgress(0.95f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
||||||
|
throw IOException(context.getString(R.string.flash_failed_message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommand(su: Boolean, cmd: String): Int {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh")
|
||||||
|
.redirectErrorStream(true)
|
||||||
|
.start()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.readText().trim()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
""
|
||||||
|
} finally {
|
||||||
|
process.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootAvailable(): Boolean {
|
||||||
|
return try {
|
||||||
|
val process = Runtime.getRuntime().exec("su -c id")
|
||||||
|
val exitValue = process.waitFor()
|
||||||
|
exitValue == 0
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HorizonKernelFlashProgress(state: FlashState) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_flash_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
progress = { state.progress },
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = state.currentStep,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.logs.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_logs_label),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Start)
|
||||||
|
.padding(top = 8.dp, bottom = 4.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = 230.dp)
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
tonalElevation = 1.dp,
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
state.logs.forEach { log ->
|
||||||
|
Text(
|
||||||
|
text = log,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.error.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = state.error,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (state.isCompleted) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CheckCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.horizon_flash_complete),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.profile
|
package com.sukisu.ultra.profile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.profile
|
package com.sukisu.ultra.profile
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
* https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h
|
||||||
333
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
333
manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Handler
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.navigation.NavBackStackEntry
|
||||||
|
import androidx.navigation.NavHostController
|
||||||
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
|
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||||
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
|
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
||||||
|
import com.ramcosta.composedestinations.spec.RouteOrDirection
|
||||||
|
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
||||||
|
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
||||||
|
import io.sukisu.ultra.UltraToolInstall
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||||
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.webui.initPlatform
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
private inner class ThemeChangeContentObserver(
|
||||||
|
handler: Handler,
|
||||||
|
private val onThemeChanged: () -> Unit
|
||||||
|
) : ContentObserver(handler) {
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
super.onChange(selfChange)
|
||||||
|
onThemeChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用保存的语言设置
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
val prefs = newBase.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
var context = newBase
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val config = Configuration(newBase.resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
context = newBase.createConfigurationContext(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
// 确保应用正确的语言设置
|
||||||
|
applyLanguageSetting()
|
||||||
|
|
||||||
|
applyCustomDpi()
|
||||||
|
|
||||||
|
// Enable edge to edge
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val isFirstRun = prefs.getBoolean("is_first_run", true)
|
||||||
|
|
||||||
|
if (isFirstRun) {
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
|
prefs.edit { putBoolean("is_first_run", false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载保存的背景设置
|
||||||
|
loadThemeMode()
|
||||||
|
loadThemeColors()
|
||||||
|
loadDynamicColorState()
|
||||||
|
CardConfig.load(applicationContext)
|
||||||
|
|
||||||
|
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) {
|
||||||
|
runOnUiThread {
|
||||||
|
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
loadCustomBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentResolver.registerContentObserver(
|
||||||
|
android.provider.Settings.System.getUriFor("ui_night_mode"),
|
||||||
|
false,
|
||||||
|
contentObserver
|
||||||
|
)
|
||||||
|
|
||||||
|
val destroyListeners = mutableListOf<() -> Unit>()
|
||||||
|
destroyListeners.add {
|
||||||
|
contentResolver.unregisterContentObserver(contentObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
if (isManager) {
|
||||||
|
install()
|
||||||
|
UltraToolInstall.tryToInstall()
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
KernelSUTheme {
|
||||||
|
val navController = rememberNavController()
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// pre-init platform to faster start WebUI X activities
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
initPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用自定义DPI设置
|
||||||
|
private fun applyCustomDpi() {
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val customDpi = prefs.getInt("app_dpi", 0)
|
||||||
|
|
||||||
|
if (customDpi > 0) {
|
||||||
|
try {
|
||||||
|
val resources = resources
|
||||||
|
val metrics = resources.displayMetrics
|
||||||
|
metrics.density = customDpi / 160f
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
metrics.scaledDensity = customDpi / 160f
|
||||||
|
metrics.densityDpi = customDpi
|
||||||
|
} 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()
|
||||||
|
applyLanguageSetting()
|
||||||
|
|
||||||
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
loadCustomBackground()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val destroyListeners = mutableListOf<() -> Unit>()
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
destroyListeners.forEach { it() }
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
applyLanguageSetting()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
// 检查是否显示KPM
|
||||||
|
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("show_kpm_info", true)
|
||||||
|
|
||||||
|
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") && showKpmInfo) {
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import shirkneko.zako.sukisu.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
@@ -98,7 +98,7 @@ private fun AboutCardContent() {
|
|||||||
val annotatedString = AnnotatedString.Companion.fromHtml(
|
val annotatedString = AnnotatedString.Companion.fromHtml(
|
||||||
htmlString = stringResource(
|
htmlString = stringResource(
|
||||||
id = R.string.about_source_code,
|
id = R.string.about_source_code,
|
||||||
"<b><a href=\"https://github.com/ShirkNeko/KernelSU\">GitHub</a></b>",
|
"<b><a href=\"https://github.com/ShirkNeko/SukiSU-Ultra\">GitHub</a></b>",
|
||||||
"<b><a href=\"https://t.me/SukiKSU\">Telegram</a></b>"
|
"<b><a href=\"https://t.me/SukiKSU\">Telegram</a></b>"
|
||||||
),
|
),
|
||||||
linkStyles = TextLinkStyles(
|
linkStyles = TextLinkStyles(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.graphics.text.LineBreaker
|
import android.graphics.text.LineBreaker
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.detectTransformGestures
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ImageEditorDialog(
|
||||||
|
imageUri: Uri,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: (Uri) -> Unit
|
||||||
|
) {
|
||||||
|
var scale by remember { mutableFloatStateOf(1f) }
|
||||||
|
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||||
|
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||||
|
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
|
var imageSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
var screenSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
val animatedScale by animateFloatAsState(
|
||||||
|
targetValue = scale,
|
||||||
|
label = "ScaleAnimation"
|
||||||
|
)
|
||||||
|
val animatedOffsetX by animateFloatAsState(
|
||||||
|
targetValue = offsetX,
|
||||||
|
label = "OffsetXAnimation"
|
||||||
|
)
|
||||||
|
val animatedOffsetY by animateFloatAsState(
|
||||||
|
targetValue = offsetY,
|
||||||
|
label = "OffsetYAnimation"
|
||||||
|
)
|
||||||
|
val updateTransformation = remember {
|
||||||
|
{ newScale: Float, newOffsetX: Float, newOffsetY: Float ->
|
||||||
|
val scaleDiff = kotlin.math.abs(newScale - lastScale)
|
||||||
|
val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX)
|
||||||
|
val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY)
|
||||||
|
if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) {
|
||||||
|
scale = newScale
|
||||||
|
offsetX = newOffsetX
|
||||||
|
offsetY = newOffsetY
|
||||||
|
lastScale = newScale
|
||||||
|
lastOffsetX = newOffsetX
|
||||||
|
lastOffsetY = newOffsetY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val scaleToFullScreen = remember {
|
||||||
|
{
|
||||||
|
if (imageSize.height > 0 && screenSize.height > 0) {
|
||||||
|
val newScale = screenSize.height / imageSize.height
|
||||||
|
updateTransformation(newScale, 0f, 0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = true,
|
||||||
|
dismissOnClickOutside = false,
|
||||||
|
usePlatformDefaultWidth = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.9f))
|
||||||
|
.onSizeChanged { size ->
|
||||||
|
screenSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(imageUri)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = stringResource(R.string.settings_custom_background),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.graphicsLayer(
|
||||||
|
scaleX = animatedScale,
|
||||||
|
scaleY = animatedScale,
|
||||||
|
translationX = animatedOffsetX,
|
||||||
|
translationY = animatedOffsetY
|
||||||
|
)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTransformGestures { _, pan, zoom, _ ->
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val newScale = (scale * zoom).coerceIn(0.5f, 3f)
|
||||||
|
val maxOffsetX = max(0f, size.width * (newScale - 1) / 2)
|
||||||
|
val maxOffsetY = max(0f, size.height * (newScale - 1) / 2)
|
||||||
|
val newOffsetX = if (maxOffsetX > 0) {
|
||||||
|
(offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
val newOffsetY = if (maxOffsetY > 0) {
|
||||||
|
(offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onSizeChanged { size ->
|
||||||
|
imageSize = Size(size.width.toFloat(), size.height.toFloat())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.TopCenter),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onDismiss,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = stringResource(R.string.cancel),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { scaleToFullScreen() },
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Fullscreen,
|
||||||
|
contentDescription = stringResource(R.string.reprovision),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||||
|
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||||
|
savedUri?.let { onConfirm(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = stringResource(R.string.confirm),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.image_editor_hint),
|
||||||
|
color = Color.White,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ksuApp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun KsuIsValid(
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val isManager = Natives.becomeManager(ksuApp.packageName)
|
||||||
|
val ksuVersion = if (isManager) Natives.version else null
|
||||||
|
|
||||||
|
if (ksuVersion != null) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
@@ -42,7 +42,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
|
||||||
private const val TAG = "SearchBar"
|
private const val TAG = "SearchBar"
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ fun SearchAppBar(
|
|||||||
var onSearch by remember { mutableStateOf(false) }
|
var onSearch by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 获取卡片颜色和透明度
|
// 获取卡片颜色和透明度
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
if (onSearch) {
|
if (onSearch) {
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import com.dergoogler.mmrl.ui.component.LabelItem
|
||||||
|
import com.dergoogler.mmrl.ui.component.text.TextRow
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchItem(
|
fun SwitchItem(
|
||||||
@@ -21,9 +25,11 @@ fun SwitchItem(
|
|||||||
summary: String? = null,
|
summary: String? = null,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
onCheckedChange: (Boolean) -> Unit
|
beta: Boolean = false,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -36,10 +42,31 @@ fun SwitchItem(
|
|||||||
onValueChange = onCheckedChange
|
onValueChange = onCheckedChange
|
||||||
),
|
),
|
||||||
headlineContent = {
|
headlineContent = {
|
||||||
Text(title)
|
TextRow(
|
||||||
|
leadingContent = if (beta) {
|
||||||
|
{
|
||||||
|
LabelItem(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = "Beta"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = title,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
leadingContent = icon?.let {
|
leadingContent = icon?.let {
|
||||||
{ Icon(icon, title) }
|
{
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = title,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
@@ -51,7 +78,10 @@ fun SwitchItem(
|
|||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
Text(summary)
|
Text(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = summary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.SdStorage
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 槽位选择对话框组件
|
||||||
|
* 用于Kernel刷写时选择目标槽位
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SlotSelectionDialog(
|
||||||
|
show: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSlotSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||||
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
try {
|
||||||
|
currentSlot = getCurrentSlot(context)
|
||||||
|
errorMessage = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
errorMessage = e.message
|
||||||
|
currentSlot = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.select_slot_title),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
if (errorMessage != null) {
|
||||||
|
Text(
|
||||||
|
text = "Error: $errorMessage",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.current_slot,
|
||||||
|
currentSlot ?: "Unknown"
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.select_slot_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Horizontal arrangement for slot options with highlighted current slot
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
val slotOptions = listOf(
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(id = R.string.slot_a),
|
||||||
|
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null,
|
||||||
|
icon = Icons.Filled.SdStorage
|
||||||
|
),
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(id = R.string.slot_b),
|
||||||
|
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null,
|
||||||
|
icon = Icons.Filled.SdStorage
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
slotOptions.forEachIndexed { index, option ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(
|
||||||
|
color = if (option.subtitleText != null) {
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
onSlotSelected(
|
||||||
|
when (index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = option.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (option.subtitleText != null) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = option.titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (option.subtitleText != null) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
)
|
||||||
|
option.subtitleText?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (true) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
currentSlot?.let { onSlotSelected(it) }
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.ok),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = onDismiss
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = cardColor,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
tonalElevation = 4.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data class for list options
|
||||||
|
data class ListOption(
|
||||||
|
val titleText: String,
|
||||||
|
val subtitleText: String?,
|
||||||
|
val icon: ImageVector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Utility function to get current slot
|
||||||
|
private fun getCurrentSlot(context: Context): String? {
|
||||||
|
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||||
|
if (it.startsWith("_")) it.substring(1) else it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runCommandGetOutput(su: Boolean, cmd: String): String? {
|
||||||
|
return try {
|
||||||
|
val process = ProcessBuilder(if (su) "su" else "sh").start()
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("$cmd\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
writer.flush()
|
||||||
|
}
|
||||||
|
process.inputStream.bufferedReader().use { reader ->
|
||||||
|
reader.readText().trim()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
// 颜色动画
|
||||||
|
val iconTint by animateColorAsState(
|
||||||
|
targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
|
||||||
|
animationSpec = tween(300),
|
||||||
|
label = "iconTint"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 开关颜色
|
||||||
|
val switchColors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
checkedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||||
|
disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f),
|
||||||
|
disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
maxLines = Int.MAX_VALUE,
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component.profile
|
package com.sukisu.ultra.ui.component.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppProfileConfig(
|
fun AppProfileConfig(
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component.profile
|
package com.sukisu.ultra.ui.component.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -42,12 +42,12 @@ import com.maxkeppeler.sheets.input.models.ValidationResult
|
|||||||
import com.maxkeppeler.sheets.list.ListDialog
|
import com.maxkeppeler.sheets.list.ListDialog
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.profile.Capabilities
|
import com.sukisu.ultra.profile.Capabilities
|
||||||
import shirkneko.zako.sukisu.profile.Groups
|
import com.sukisu.ultra.profile.Groups
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
import shirkneko.zako.sukisu.ui.util.isSepolicyValid
|
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component.profile
|
package com.sukisu.ultra.ui.component.profile
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -23,11 +23,11 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.util.listAppProfileTemplates
|
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||||
import shirkneko.zako.sukisu.ui.util.setSepolicy
|
import com.sukisu.ultra.ui.util.setSepolicy
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.getTemplateInfoById
|
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -0,0 +1,593 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.AccountCircle
|
||||||
|
import androidx.compose.material.icons.filled.Android
|
||||||
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarColors
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
|
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||||
|
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||||
|
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
|
import com.sukisu.ultra.ui.util.forceStopApp
|
||||||
|
import com.sukisu.ultra.ui.util.getSepolicy
|
||||||
|
import com.sukisu.ultra.ui.util.launchApp
|
||||||
|
import com.sukisu.ultra.ui.util.restartApp
|
||||||
|
import com.sukisu.ultra.ui.util.setSepolicy
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author weishu
|
||||||
|
* @date 2023/5/16.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun AppProfileScreen(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
appInfo: SuperUserViewModel.AppInfo,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
||||||
|
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
||||||
|
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
||||||
|
|
||||||
|
val packageName = appInfo.packageName
|
||||||
|
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
||||||
|
if (initialProfile.allowSu) {
|
||||||
|
initialProfile.rules = getSepolicy(packageName)
|
||||||
|
}
|
||||||
|
var profile by rememberSaveable {
|
||||||
|
mutableStateOf(initialProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
title = appInfo.label,
|
||||||
|
packageName = packageName,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
|
) { paddingValues ->
|
||||||
|
AppProfileInner(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
packageName = appInfo.packageName,
|
||||||
|
appLabel = appInfo.label,
|
||||||
|
appIcon = {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
||||||
|
contentDescription = appInfo.label,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.width(48.dp)
|
||||||
|
.height(48.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
profile = profile,
|
||||||
|
onViewTemplate = {
|
||||||
|
getTemplateInfoById(it)?.let { info ->
|
||||||
|
navigator.navigate(TemplateEditorScreenDestination(info))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onManageTemplate = {
|
||||||
|
navigator.navigate(AppProfileTemplateScreenDestination())
|
||||||
|
},
|
||||||
|
onProfileChange = {
|
||||||
|
scope.launch {
|
||||||
|
if (it.allowSu) {
|
||||||
|
// sync with allowlist.c - forbid_system_uid
|
||||||
|
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
||||||
|
snackBarHost.showSnackbar(suNotAllowed)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
||||||
|
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!Natives.setAppProfile(it)) {
|
||||||
|
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
||||||
|
} else {
|
||||||
|
profile = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppProfileInner(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
packageName: String,
|
||||||
|
appLabel: String,
|
||||||
|
appIcon: @Composable () -> Unit,
|
||||||
|
profile: Natives.Profile,
|
||||||
|
onViewTemplate: (id: String) -> Unit = {},
|
||||||
|
onManageTemplate: () -> Unit = {},
|
||||||
|
onProfileChange: (Natives.Profile) -> Unit,
|
||||||
|
) {
|
||||||
|
val isRootGranted = profile.allowSu
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
AppMenuBox(packageName) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = appLabel,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = packageName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = appIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.Security,
|
||||||
|
title = stringResource(id = R.string.superuser),
|
||||||
|
checked = isRootGranted,
|
||||||
|
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Crossfade(
|
||||||
|
targetState = isRootGranted,
|
||||||
|
label = "RootAccess"
|
||||||
|
) { current ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||||
|
) {
|
||||||
|
if (current) {
|
||||||
|
val initialMode = if (profile.rootUseDefault) {
|
||||||
|
Mode.Default
|
||||||
|
} else if (profile.rootTemplate != null) {
|
||||||
|
Mode.Template
|
||||||
|
} else {
|
||||||
|
Mode.Custom
|
||||||
|
}
|
||||||
|
var mode by rememberSaveable {
|
||||||
|
mutableStateOf(initialMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
ProfileBox(mode, true) {
|
||||||
|
// template mode shouldn't change profile here!
|
||||||
|
if (it == Mode.Default || it == Mode.Custom) {
|
||||||
|
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
||||||
|
}
|
||||||
|
mode = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = mode != Mode.Default,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Crossfade(targetState = mode, label = "ProfileMode") { currentMode ->
|
||||||
|
when (currentMode) {
|
||||||
|
Mode.Template -> {
|
||||||
|
TemplateConfig(
|
||||||
|
profile = profile,
|
||||||
|
onViewTemplate = onViewTemplate,
|
||||||
|
onManageTemplate = onManageTemplate,
|
||||||
|
onProfileChange = onProfileChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Mode.Custom -> {
|
||||||
|
RootProfileConfig(
|
||||||
|
fixedName = true,
|
||||||
|
profile = profile,
|
||||||
|
onProfileChange = onProfileChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
ProfileBox(mode, false) {
|
||||||
|
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = mode == Mode.Custom,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
AppProfileConfig(
|
||||||
|
fixedName = true,
|
||||||
|
profile = profile,
|
||||||
|
enabled = mode == Mode.Custom,
|
||||||
|
onProfileChange = onProfileChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class Mode(@StringRes private val res: Int) {
|
||||||
|
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
||||||
|
|
||||||
|
val text: String
|
||||||
|
@Composable get() = stringResource(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
title: String,
|
||||||
|
packageName: String,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
colors: TopAppBarColors,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = packageName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.alpha(0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = colors,
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = onBack,
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
modifier = Modifier.shadow(
|
||||||
|
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||||
|
4.dp else 0.dp,
|
||||||
|
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ProfileBox(
|
||||||
|
mode: Mode,
|
||||||
|
hasTemplate: Boolean,
|
||||||
|
onModeChange: (Mode) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = mode.text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.AccountCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = Dp.Hairline,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
selected = mode == Mode.Default,
|
||||||
|
onClick = { onModeChange(Mode.Default) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_default),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasTemplate) {
|
||||||
|
FilterChip(
|
||||||
|
selected = mode == Mode.Template,
|
||||||
|
onClick = { onModeChange(Mode.Template) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_template),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterChip(
|
||||||
|
selected = mode == Mode.Custom,
|
||||||
|
onClick = { onModeChange(Mode.Custom) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_custom),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||||
|
@Composable
|
||||||
|
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||||
|
val density = LocalDensity.current
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = {
|
||||||
|
touchPoint = it
|
||||||
|
expanded = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
|
||||||
|
val (offsetX, offsetY) = with(density) {
|
||||||
|
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
offset = DpOffset(offsetX, -offsetY),
|
||||||
|
onDismissRequest = {
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.launch_app),
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
launchApp(packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.force_stop_app),
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
forceStopApp(packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.restart_app),
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
restartApp(packageName)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AppProfilePreview() {
|
||||||
|
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||||
|
Surface {
|
||||||
|
AppProfileInner(
|
||||||
|
packageName = "icu.nullptr.test",
|
||||||
|
appLabel = "Test",
|
||||||
|
appIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Android,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
profile = profile,
|
||||||
|
onProfileChange = {
|
||||||
|
profile = it
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
@@ -11,7 +11,7 @@ import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDe
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
enum class BottomBarDestination(
|
enum class BottomBarDestination(
|
||||||
val direction: DirectionDestinationSpec,
|
val direction: DirectionDestinationSpec,
|
||||||
@@ -21,8 +21,8 @@ enum class BottomBarDestination(
|
|||||||
val rootRequired: Boolean,
|
val rootRequired: Boolean,
|
||||||
) {
|
) {
|
||||||
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
|
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),
|
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
||||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
||||||
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
|
|
||||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -37,10 +37,10 @@ import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import shirkneko.zako.sukisu.ui.util.runModuleAction
|
import com.sukisu.ultra.ui.util.runModuleAction
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
@@ -30,9 +30,9 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import shirkneko.zako.sukisu.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.util.*
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
import com.sukisu.ultra.R
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
@@ -45,10 +45,6 @@ enum class FlashingStatus {
|
|||||||
|
|
||||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||||
|
|
||||||
fun getFlashingStatus(): FlashingStatus {
|
|
||||||
return currentFlashingStatus.value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setFlashingStatus(status: FlashingStatus) {
|
fun setFlashingStatus(status: FlashingStatus) {
|
||||||
currentFlashingStatus.value = status
|
currentFlashingStatus.value = status
|
||||||
}
|
}
|
||||||
1036
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
1036
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt
Normal file
File diff suppressed because it is too large
Load Diff
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
788
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt
Normal file
@@ -0,0 +1,788 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.AutoFixHigh
|
||||||
|
import androidx.compose.material.icons.filled.FileUpload
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.RadioButtonDefaults
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.Header
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
||||||
|
import com.maxkeppeler.sheets.list.ListDialog
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelState
|
||||||
|
import com.sukisu.ultra.flash.HorizonKernelWorker
|
||||||
|
import com.sukisu.ultra.ui.component.DialogHandle
|
||||||
|
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||||
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
|
import com.sukisu.ultra.ui.util.LkmSelection
|
||||||
|
import com.sukisu.ultra.ui.util.getCurrentKmi
|
||||||
|
import com.sukisu.ultra.ui.util.getSupportedKmis
|
||||||
|
import com.sukisu.ultra.ui.util.isAbDevice
|
||||||
|
import com.sukisu.ultra.ui.util.isInitBoot
|
||||||
|
import com.sukisu.ultra.ui.util.rootAvailable
|
||||||
|
import com.sukisu.ultra.getKernelVersion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author weishu
|
||||||
|
* @date 2024/3/12.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun InstallScreen(navigator: DestinationsNavigator) {
|
||||||
|
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||||
|
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
|
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||||
|
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
val horizonKernelState = remember { HorizonKernelState() }
|
||||||
|
val flashState by horizonKernelState.state.collectAsState()
|
||||||
|
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
val kernelVersion = getKernelVersion()
|
||||||
|
val isGKI = kernelVersion.isGKI()
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
|
|
||||||
|
val onFlashComplete = {
|
||||||
|
showRebootDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRebootDialog) {
|
||||||
|
RebootDialog(
|
||||||
|
show = true,
|
||||||
|
onDismiss = { showRebootDialog = false },
|
||||||
|
onConfirm = {
|
||||||
|
showRebootDialog = false
|
||||||
|
try {
|
||||||
|
val process = Runtime.getRuntime().exec("su")
|
||||||
|
process.outputStream.bufferedWriter().use { writer ->
|
||||||
|
writer.write("svc power reboot\n")
|
||||||
|
writer.write("exit\n")
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val onInstall = {
|
||||||
|
installMethod?.let { method ->
|
||||||
|
when (method) {
|
||||||
|
is InstallMethod.HorizonKernel -> {
|
||||||
|
method.uri?.let { uri ->
|
||||||
|
val worker = HorizonKernelWorker(
|
||||||
|
context = context,
|
||||||
|
state = horizonKernelState,
|
||||||
|
slot = method.slot
|
||||||
|
)
|
||||||
|
worker.uri = uri
|
||||||
|
worker.setOnFlashCompleteListener(onFlashComplete)
|
||||||
|
worker.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val flashIt = FlashIt.FlashBoot(
|
||||||
|
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
||||||
|
lkm = lkmSelection,
|
||||||
|
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
||||||
|
)
|
||||||
|
navigator.navigate(FlashScreenDestination(flashIt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
// 槽位选择
|
||||||
|
SlotSelectionDialog(
|
||||||
|
show = showSlotSelectionDialog && isAbDevice,
|
||||||
|
onDismiss = { showSlotSelectionDialog = false },
|
||||||
|
onSlotSelected = { slot ->
|
||||||
|
showSlotSelectionDialog = false
|
||||||
|
val horizonMethod = InstallMethod.HorizonKernel(
|
||||||
|
uri = tempKernelUri,
|
||||||
|
slot = slot,
|
||||||
|
summary = summary
|
||||||
|
)
|
||||||
|
installMethod = horizonMethod
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val currentKmi by produceState(initialValue = "") {
|
||||||
|
value = getCurrentKmi()
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
||||||
|
kmi?.let {
|
||||||
|
lkmSelection = LkmSelection.KmiString(it)
|
||||||
|
onInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onClickNext = {
|
||||||
|
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
||||||
|
selectKmiDialog.show()
|
||||||
|
} else {
|
||||||
|
onInstall()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectLkmLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
it.data?.data?.let { uri ->
|
||||||
|
lkmSelection = LkmSelection.LkmUri(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onLkmUpload = {
|
||||||
|
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/octet-stream"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
onBack = { navigator.popBackStack() },
|
||||||
|
onLkmUpload = onLkmUpload,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
)
|
||||||
|
) { innerPadding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(innerPadding)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
) {
|
||||||
|
SelectInstallMethod(
|
||||||
|
isGKI = isGKI,
|
||||||
|
isAbDevice = isAbDevice,
|
||||||
|
onSelected = { method ->
|
||||||
|
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||||
|
if (isAbDevice) {
|
||||||
|
tempKernelUri = method.uri
|
||||||
|
showSlotSelectionDialog = true
|
||||||
|
} else {
|
||||||
|
installMethod = method
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
installMethod = method
|
||||||
|
}
|
||||||
|
horizonKernelState.reset()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
HorizonKernelFlashProgress(flashState)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.selected_lkm,
|
||||||
|
it.uri.lastPathSegment ?: "(file)"
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||||
|
if (method.slot != null) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = R.string.selected_slot,
|
||||||
|
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||||
|
else stringResource(id = R.string.slot_b)
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = installMethod != null && !flashState.isFlashing,
|
||||||
|
onClick = onClickNext,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.install_next),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RebootDialog(
|
||||||
|
show: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onConfirm: () -> Unit
|
||||||
|
) {
|
||||||
|
if (show) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
||||||
|
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onConfirm) {
|
||||||
|
Text(stringResource(id = R.string.yes))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(id = R.string.no))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class InstallMethod {
|
||||||
|
data class SelectFile(
|
||||||
|
val uri: Uri? = null,
|
||||||
|
@StringRes override val label: Int = R.string.select_file,
|
||||||
|
override val summary: String?
|
||||||
|
) : InstallMethod()
|
||||||
|
|
||||||
|
data object DirectInstall : InstallMethod() {
|
||||||
|
override val label: Int
|
||||||
|
get() = R.string.direct_install
|
||||||
|
}
|
||||||
|
|
||||||
|
data object DirectInstallToInactiveSlot : InstallMethod() {
|
||||||
|
override val label: Int
|
||||||
|
get() = R.string.install_inactive_slot
|
||||||
|
}
|
||||||
|
|
||||||
|
data class HorizonKernel(
|
||||||
|
val uri: Uri? = null,
|
||||||
|
val slot: String? = null,
|
||||||
|
@StringRes override val label: Int = R.string.horizon_kernel,
|
||||||
|
override val summary: String? = null
|
||||||
|
) : InstallMethod()
|
||||||
|
|
||||||
|
abstract val label: Int
|
||||||
|
open val summary: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SelectInstallMethod(
|
||||||
|
isGKI: Boolean = false,
|
||||||
|
isAbDevice: Boolean = false,
|
||||||
|
onSelected: (InstallMethod) -> Unit = {}
|
||||||
|
) {
|
||||||
|
val rootAvailable = rootAvailable()
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
|
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
val selectFileTip = stringResource(
|
||||||
|
id = R.string.select_file_tip,
|
||||||
|
if (isInitBoot()) "init_boot" else "boot"
|
||||||
|
)
|
||||||
|
|
||||||
|
val radioOptions = mutableListOf<InstallMethod>(
|
||||||
|
InstallMethod.SelectFile(summary = selectFileTip)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rootAvailable) {
|
||||||
|
radioOptions.add(InstallMethod.DirectInstall)
|
||||||
|
if (isAbDevice) {
|
||||||
|
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
||||||
|
}
|
||||||
|
radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary))
|
||||||
|
}
|
||||||
|
|
||||||
|
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
||||||
|
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
||||||
|
|
||||||
|
val selectImageLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) {
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
it.data?.data?.let { uri ->
|
||||||
|
val option = when (currentSelectingMethod) {
|
||||||
|
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||||
|
uri,
|
||||||
|
summary = selectFileTip
|
||||||
|
)
|
||||||
|
|
||||||
|
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||||
|
uri,
|
||||||
|
summary = horizonKernelSummary
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
option?.let {
|
||||||
|
selectedOption = it
|
||||||
|
onSelected(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val confirmDialog = rememberConfirmDialog(
|
||||||
|
onConfirm = {
|
||||||
|
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
||||||
|
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
||||||
|
},
|
||||||
|
onDismiss = null
|
||||||
|
)
|
||||||
|
|
||||||
|
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
||||||
|
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
||||||
|
|
||||||
|
val onClick = { option: InstallMethod ->
|
||||||
|
currentSelectingMethod = option
|
||||||
|
when (option) {
|
||||||
|
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||||
|
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/*"
|
||||||
|
putExtra(
|
||||||
|
Intent.EXTRA_MIME_TYPES,
|
||||||
|
arrayOf("application/octet-stream", "application/zip")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
is InstallMethod.DirectInstall -> {
|
||||||
|
selectedOption = option
|
||||||
|
onSelected(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||||
|
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var LKMExpanded by remember { mutableStateOf(false) }
|
||||||
|
var GKIExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
// LKM 安装/修补
|
||||||
|
if (isGKI) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.AutoFixHigh,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.Lkm_install_methods),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
LKMExpanded = !LKMExpanded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = LKMExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
radioOptions.take(3).forEach { option ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Surface(
|
||||||
|
color = if (option.javaClass == selectedOption?.javaClass)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(
|
||||||
|
value = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onValueChange = { onClick(option) },
|
||||||
|
role = Role.RadioButton,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onClick = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = option.label),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
option.summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// anykernel3 刷写
|
||||||
|
if (rootAvailable) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.FileUpload,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.GKI_install_methods),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
GKIExpanded = !GKIExpanded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = GKIExpanded,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Surface(
|
||||||
|
color = if (option.javaClass == selectedOption?.javaClass)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(
|
||||||
|
value = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onValueChange = { onClick(option) },
|
||||||
|
role = Role.RadioButton,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = option.javaClass == selectedOption?.javaClass,
|
||||||
|
onClick = null,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = option.label),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
option.summary?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
||||||
|
return rememberCustomDialog { dismiss ->
|
||||||
|
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||||
|
value = getSupportedKmis()
|
||||||
|
}
|
||||||
|
val options = supportedKmi.map { value ->
|
||||||
|
ListOption(
|
||||||
|
titleText = value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection by remember { mutableStateOf<String?>(null) }
|
||||||
|
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = backgroundColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
|
onSelected(selection)
|
||||||
|
}, onCloseRequest = {
|
||||||
|
dismiss()
|
||||||
|
}), header = Header.Default(
|
||||||
|
title = stringResource(R.string.select_kmi),
|
||||||
|
), selection = ListSelection.Single(
|
||||||
|
showRadioButtons = true,
|
||||||
|
options = options,
|
||||||
|
) { _, option ->
|
||||||
|
selection = option.titleText
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onLkmUpload: () -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = cardAlpha
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.install),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SelectInstallPreview() {
|
||||||
|
InstallScreen(EmptyDestinationsNavigator)
|
||||||
|
}
|
||||||
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
783
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt
Normal file
@@ -0,0 +1,783 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.ui.component.*
|
||||||
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.KpmViewModel
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
import java.io.File
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.*
|
||||||
|
import android.app.Activity
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KPM 管理界面
|
||||||
|
* 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能
|
||||||
|
* 开发者:ShirkNeko, Liaokong
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun KpmScreen(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
viewModel: KpmViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
|
val confirmDialog = rememberConfirmDialog()
|
||||||
|
|
||||||
|
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||||
|
val moduleFileName = module.id
|
||||||
|
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
|
val kpmInstallSuccess = stringResource(R.string.kpm_install_success)
|
||||||
|
val kpmInstallFailed = stringResource(R.string.kpm_install_failed)
|
||||||
|
val cancel = stringResource(R.string.cancel)
|
||||||
|
val uninstall = stringResource(R.string.uninstall)
|
||||||
|
val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file)
|
||||||
|
val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success)
|
||||||
|
val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed)
|
||||||
|
val kpmInstallMode = stringResource(R.string.kpm_install_mode)
|
||||||
|
val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load)
|
||||||
|
val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed)
|
||||||
|
val invalidFileTypeMessage = stringResource(R.string.invalid_file_type)
|
||||||
|
val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename)
|
||||||
|
|
||||||
|
var tempFileForInstall by remember { mutableStateOf<File?>(null) }
|
||||||
|
val installModeDialog = rememberCustomDialog { dismiss ->
|
||||||
|
var moduleName by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(tempFileForInstall) {
|
||||||
|
tempFileForInstall?.let { tempFile ->
|
||||||
|
try {
|
||||||
|
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
var line: String?
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (line!!.startsWith("name=")) {
|
||||||
|
moduleName = line.substringAfter("name=").trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.delete()
|
||||||
|
tempFileForInstall = null
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = kpmInstallMode,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
moduleName?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.let { tempFile ->
|
||||||
|
handleModuleInstall(
|
||||||
|
tempFile = tempFile,
|
||||||
|
isEmbed = false,
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackBarHost = snackBarHost,
|
||||||
|
kpmInstallSuccess = kpmInstallSuccess,
|
||||||
|
kpmInstallFailed = kpmInstallFailed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFileForInstall = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Download,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
Text(kpmInstallModeLoad)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.let { tempFile ->
|
||||||
|
handleModuleInstall(
|
||||||
|
tempFile = tempFile,
|
||||||
|
isEmbed = true,
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackBarHost = snackBarHost,
|
||||||
|
kpmInstallSuccess = kpmInstallSuccess,
|
||||||
|
kpmInstallFailed = kpmInstallFailed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFileForInstall = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Inventory,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
Text(kpmInstallModeEmbed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
tempFileForInstall?.delete()
|
||||||
|
tempFileForInstall = null
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(cancel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
|
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
|
scope.launch {
|
||||||
|
val fileName = uri.lastPathSegment ?: "unknown.kpm"
|
||||||
|
val encodedFileName = URLEncoder.encode(fileName, "UTF-8")
|
||||||
|
val tempFile = File(context.cacheDir, encodedFileName)
|
||||||
|
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream")
|
||||||
|
|
||||||
|
if (!isCorrectMimeType) {
|
||||||
|
var shouldShowSnackbar = true
|
||||||
|
try {
|
||||||
|
val matchCount = checkStringsCommand(tempFile)
|
||||||
|
val isElf = isElfFile(tempFile)
|
||||||
|
|
||||||
|
if (matchCount >= 1 || isElf) {
|
||||||
|
shouldShowSnackbar = false
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to execute checks: ${e.message}", e)
|
||||||
|
}
|
||||||
|
if (shouldShowSnackbar) {
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = invalidFileTypeMessage,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFile.delete()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
tempFileForInstall = tempFile
|
||||||
|
installModeDialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while(true) {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
delay(5000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE)
|
||||||
|
var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
SearchAppBar(
|
||||||
|
title = { Text(stringResource(R.string.kpm_title)) },
|
||||||
|
searchText = viewModel.search,
|
||||||
|
onSearchTextChange = { viewModel.search = it },
|
||||||
|
onClearClick = { viewModel.search = "" },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
dropdownContent = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.fetchModuleList() }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = stringResource(R.string.refresh),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
selectPatchLauncher.launch(
|
||||||
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
|
type = "application/octet-stream"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Add,
|
||||||
|
contentDescription = stringResource(R.string.kpm_install),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_install),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
expanded = true,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
|
) { padding ->
|
||||||
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
|
if (!isNoticeClosed) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kernel_module_notice),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
isNoticeClosed = true
|
||||||
|
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(R.string.close_notice)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.moduleList.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Code,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.kpm_empty),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(viewModel.moduleList) { module ->
|
||||||
|
KpmModuleItem(
|
||||||
|
module = module,
|
||||||
|
onUninstall = {
|
||||||
|
scope.launch {
|
||||||
|
val confirmContent = moduleConfirmContentMap[module.id] ?: ""
|
||||||
|
handleModuleUninstall(
|
||||||
|
module = module,
|
||||||
|
viewModel = viewModel,
|
||||||
|
snackBarHost = snackBarHost,
|
||||||
|
kpmUninstallSuccess = kpmUninstallSuccess,
|
||||||
|
kpmUninstallFailed = kpmUninstallFailed,
|
||||||
|
failedToCheckModuleFile = failedToCheckModuleFile,
|
||||||
|
uninstall = uninstall,
|
||||||
|
cancel = cancel,
|
||||||
|
confirmDialog = confirmDialog,
|
||||||
|
confirmTitle = confirmTitle,
|
||||||
|
confirmContent = confirmContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onControl = {
|
||||||
|
viewModel.loadModuleDetail(module.id)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleModuleInstall(
|
||||||
|
tempFile: File,
|
||||||
|
isEmbed: Boolean,
|
||||||
|
viewModel: KpmViewModel,
|
||||||
|
snackBarHost: SnackbarHostState,
|
||||||
|
kpmInstallSuccess: String,
|
||||||
|
kpmInstallFailed: String
|
||||||
|
) {
|
||||||
|
var moduleId: String? = null
|
||||||
|
try {
|
||||||
|
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
var line: String?
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (line!!.startsWith("name=")) {
|
||||||
|
moduleId = line.substringAfter("name=").trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moduleId == null || moduleId.isEmpty()) {
|
||||||
|
Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}")
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
tempFile.delete()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetPath = "/data/adb/kpm/$moduleId.kpm"
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEmbed) {
|
||||||
|
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
||||||
|
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||||
|
if (loadResult.startsWith("Error")) {
|
||||||
|
Log.e("KsuCli", "Failed to load KPM module: $loadResult")
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallSuccess,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e)
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmInstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun handleModuleUninstall(
|
||||||
|
module: KpmViewModel.ModuleInfo,
|
||||||
|
viewModel: KpmViewModel,
|
||||||
|
snackBarHost: SnackbarHostState,
|
||||||
|
kpmUninstallSuccess: String,
|
||||||
|
kpmUninstallFailed: String,
|
||||||
|
failedToCheckModuleFile: String,
|
||||||
|
uninstall: String,
|
||||||
|
cancel: String,
|
||||||
|
confirmTitle : String,
|
||||||
|
confirmContent : String,
|
||||||
|
confirmDialog: ConfirmDialogHandle
|
||||||
|
) {
|
||||||
|
val moduleFileName = "${module.id}.kpm"
|
||||||
|
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||||
|
|
||||||
|
val fileExists = try {
|
||||||
|
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
||||||
|
result
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = failedToCheckModuleFile,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
|
title = confirmTitle,
|
||||||
|
content = confirmContent,
|
||||||
|
confirm = uninstall,
|
||||||
|
dismiss = cancel
|
||||||
|
)
|
||||||
|
|
||||||
|
if (confirmResult == ConfirmResult.Confirmed) {
|
||||||
|
try {
|
||||||
|
val unloadResult = unloadKpmModule(module.id)
|
||||||
|
if (unloadResult.startsWith("Error")) {
|
||||||
|
Log.e("KsuCli", "Failed to unload KPM module: $unloadResult")
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallSuccess,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e)
|
||||||
|
snackBarHost.showSnackbar(
|
||||||
|
message = kpmUninstallFailed,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KpmModuleItem(
|
||||||
|
module: KpmViewModel.ModuleInfo,
|
||||||
|
onUninstall: () -> Unit,
|
||||||
|
onControl: () -> Unit
|
||||||
|
) {
|
||||||
|
val viewModel: KpmViewModel = viewModel()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
|
val successMessage = stringResource(R.string.kpm_control_success)
|
||||||
|
val failureMessage = stringResource(R.string.kpm_control_failed)
|
||||||
|
|
||||||
|
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { viewModel.hideInputDialog() },
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_control),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = viewModel.inputArgs,
|
||||||
|
onValueChange = { viewModel.updateInputArgs(it) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_args),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = module.args,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = OutlinedTextFieldDefaults.colors(
|
||||||
|
focusedBorderColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unfocusedBorderColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val result = viewModel.executeControl()
|
||||||
|
val message = when (result) {
|
||||||
|
0 -> successMessage
|
||||||
|
else -> failureMessage
|
||||||
|
}
|
||||||
|
snackBarHost.showSnackbar(message)
|
||||||
|
onControl()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.confirm),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(20.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = module.name,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = module.description,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = { viewModel.showInputDialog(module.id) },
|
||||||
|
enabled = module.hasAction,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Settings,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.kpm_control))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onUninstall,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Delete,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(stringResource(R.string.kpm_uninstall))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkStringsCommand(tempFile: File): Int {
|
||||||
|
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='")
|
||||||
|
val process = Runtime.getRuntime().exec(command)
|
||||||
|
val inputStream = process.inputStream
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
var line: String?
|
||||||
|
var matchCount = 0
|
||||||
|
val keywords = listOf("name=", "version=", "license=", "author=")
|
||||||
|
var nameExists = false
|
||||||
|
|
||||||
|
while (reader.readLine().also { line = it } != null) {
|
||||||
|
if (!nameExists && line!!.startsWith("name=")) {
|
||||||
|
nameExists = true
|
||||||
|
matchCount++
|
||||||
|
} else if (nameExists) {
|
||||||
|
for (keyword in keywords) {
|
||||||
|
if (line!!.startsWith(keyword)) {
|
||||||
|
matchCount++
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process.waitFor()
|
||||||
|
|
||||||
|
return if (nameExists) matchCount else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isElfFile(tempFile: File): Boolean {
|
||||||
|
val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte())
|
||||||
|
val fileBytes = ByteArray(4)
|
||||||
|
FileInputStream(tempFile).use { input ->
|
||||||
|
input.read(fileBytes)
|
||||||
|
}
|
||||||
|
return fileBytes.contentEquals(elfMagic)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.app.Activity.*
|
import android.app.Activity.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -9,22 +9,7 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.*
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
@@ -32,41 +17,14 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.automirrored.outlined.*
|
import androidx.compose.material.icons.automirrored.outlined.*
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ElevatedCard
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
|
||||||
import androidx.compose.material3.FilledTonalButton
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarDuration
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.SnackbarResult
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.produceState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.*
|
import androidx.compose.ui.platform.*
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -88,32 +46,33 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.ui.component.ConfirmResult
|
||||||
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberLoadingDialog
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
import com.sukisu.ultra.ui.util.DownloadListener
|
||||||
import shirkneko.zako.sukisu.ui.util.DownloadListener
|
import com.sukisu.ultra.ui.util.*
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
import com.sukisu.ultra.ui.util.download
|
||||||
import shirkneko.zako.sukisu.ui.util.download
|
import com.sukisu.ultra.ui.util.hasMagisk
|
||||||
import shirkneko.zako.sukisu.ui.util.hasMagisk
|
import com.sukisu.ultra.ui.util.reboot
|
||||||
import shirkneko.zako.sukisu.ui.util.reboot
|
import com.sukisu.ultra.ui.util.restoreModule
|
||||||
import shirkneko.zako.sukisu.ui.util.restoreModule
|
import com.sukisu.ultra.ui.util.toggleModule
|
||||||
import shirkneko.zako.sukisu.ui.util.toggleModule
|
import com.sukisu.ultra.ui.util.uninstallModule
|
||||||
import shirkneko.zako.sukisu.ui.util.uninstallModule
|
import com.sukisu.ultra.ui.webui.WebUIActivity
|
||||||
import shirkneko.zako.sukisu.ui.webui.WebUIActivity
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import shirkneko.zako.sukisu.ui.util.ModuleModify
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.ModuleViewModel
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.util.zip.ZipInputStream
|
import java.util.zip.ZipInputStream
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.webui.WebUIXActivity
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import shirkneko.zako.sukisu.ui.theme.ThemeConfig
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -239,7 +198,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
|
||||||
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
|
||||||
@@ -275,7 +234,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.MoreVert,
|
imageVector = Icons.Filled.MoreVert,
|
||||||
contentDescription = stringResource(id = R.string.settings)
|
contentDescription = stringResource(id = R.string.settings),
|
||||||
)
|
)
|
||||||
|
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
@@ -284,7 +243,16 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
text = { Text(stringResource(R.string.module_sort_action_first)) },
|
||||||
trailingIcon = { Checkbox(viewModel.sortActionFirst, null) },
|
trailingIcon = {
|
||||||
|
Checkbox(
|
||||||
|
checked = viewModel.sortActionFirst,
|
||||||
|
onCheckedChange = null,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
viewModel.sortActionFirst = !viewModel.sortActionFirst
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
@@ -300,23 +268,33 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
|
||||||
trailingIcon = { Checkbox(viewModel.sortEnabledFirst, null) },
|
trailingIcon = {
|
||||||
|
Checkbox(
|
||||||
|
checked = viewModel.sortEnabledFirst,
|
||||||
|
onCheckedChange = null,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
|
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.fetchModuleList()
|
viewModel.fetchModuleList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(stringResource(R.string.backup_modules)) },
|
text = { Text(stringResource(R.string.backup_modules)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = stringResource(R.string.backup)
|
contentDescription = stringResource(R.string.backup),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -329,7 +307,7 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Outlined.Refresh,
|
||||||
contentDescription = stringResource(R.string.restore)
|
contentDescription = stringResource(R.string.restore),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -346,7 +324,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (!hideInstallButton) {
|
if (!hideInstallButton) {
|
||||||
val moduleInstall = stringResource(id = R.string.module_install)
|
val moduleInstall = stringResource(id = R.string.module_install)
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
selectZipLauncher.launch(
|
selectZipLauncher.launch(
|
||||||
@@ -359,16 +336,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Add,
|
imageVector = Icons.Filled.Add,
|
||||||
contentDescription = moduleInstall
|
contentDescription = moduleInstall,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
text = moduleInstall
|
text = moduleInstall,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
expanded = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -385,10 +363,25 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
.padding(24.dp),
|
.padding(24.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
stringResource(R.string.module_magisk_conflict),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
textAlign = TextAlign.Center,
|
verticalArrangement = Arrangement.Center
|
||||||
)
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Warning,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(64.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.module_magisk_conflict),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -403,10 +396,17 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
|
|||||||
onClickModule = { id, name, hasWebUi ->
|
onClickModule = { id, name, hasWebUi ->
|
||||||
if (hasWebUi) {
|
if (hasWebUi) {
|
||||||
webUILauncher.launch(
|
webUILauncher.launch(
|
||||||
Intent(context, WebUIActivity::class.java)
|
if (prefs.getBoolean("use_webuix", false) && Platform.isAlive) {
|
||||||
.setData("kernelsu://webui/$id".toUri())
|
Intent(context, WebUIXActivity::class.java)
|
||||||
.putExtra("id", id)
|
.setData("kernelsu://webuix/$id".toUri())
|
||||||
.putExtra("name", name)
|
.putExtra("id", id)
|
||||||
|
.putExtra("name", name)
|
||||||
|
} else {
|
||||||
|
Intent(context, WebUIActivity::class.java)
|
||||||
|
.setData("kernelsu://webui/$id".toUri())
|
||||||
|
.putExtra("id", id)
|
||||||
|
.putExtra("name", name)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -587,10 +587,25 @@ private fun ModuleList(
|
|||||||
modifier = Modifier.fillParentMaxSize(),
|
modifier = Modifier.fillParentMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Column(
|
||||||
stringResource(R.string.module_empty),
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
textAlign = TextAlign.Center
|
verticalArrangement = Arrangement.Center
|
||||||
)
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Extension,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.module_empty),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -673,8 +688,16 @@ fun ModuleItem(
|
|||||||
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
onClick: (ModuleViewModel.ModuleInfo) -> Unit
|
||||||
) {
|
) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.shadow(
|
||||||
|
elevation = cardElevation,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
@@ -702,6 +725,7 @@ fun ModuleItem(
|
|||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
val moduleVersion = stringResource(id = R.string.module_version)
|
val moduleVersion = stringResource(id = R.string.module_version)
|
||||||
val moduleAuthor = stringResource(id = R.string.module_author)
|
val moduleAuthor = stringResource(id = R.string.module_author)
|
||||||
@@ -716,6 +740,7 @@ fun ModuleItem(
|
|||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
||||||
textDecoration = textDecoration,
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -723,7 +748,8 @@ fun ModuleItem(
|
|||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
textDecoration = textDecoration
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -731,7 +757,8 @@ fun ModuleItem(
|
|||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
||||||
textDecoration = textDecoration
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,7 +772,15 @@ fun ModuleItem(
|
|||||||
enabled = !module.update,
|
enabled = !module.update,
|
||||||
checked = module.enabled,
|
checked = module.enabled,
|
||||||
onCheckedChange = onCheckChanged,
|
onCheckedChange = onCheckChanged,
|
||||||
interactionSource = if (!module.hasWebUi) interactionSource else null
|
interactionSource = if (!module.hasWebUi) interactionSource else null,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -760,83 +795,70 @@ fun ModuleItem(
|
|||||||
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
fontWeight = MaterialTheme.typography.bodySmall.fontWeight,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
maxLines = 4,
|
maxLines = 4,
|
||||||
textDecoration = textDecoration
|
textDecoration = textDecoration,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
HorizontalDivider(thickness = Dp.Hairline)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if (module.hasActionScript) {
|
if (module.hasActionScript) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
enabled = !module.remove && module.enabled,
|
enabled = !module.remove && module.enabled,
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId))
|
||||||
viewModel.markNeedRefresh()
|
viewModel.markNeedRefresh()
|
||||||
},
|
},
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = if (!ThemeConfig.useDynamicColor) {
|
colors = ButtonDefaults.filledTonalButtonColors()
|
||||||
ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.filledTonalButtonColors()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.Outlined.PlayArrow,
|
imageVector = Icons.Outlined.PlayArrow,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasWebUi && updateUrl.isEmpty()) {
|
//if (!module.hasWebUi && updateUrl.isEmpty()) {
|
||||||
Text(
|
//Text(
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
text = stringResource(R.string.action),
|
// text = stringResource(R.string.action),
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize
|
||||||
)
|
//)
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (module.hasWebUi) {
|
if (module.hasWebUi) {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
enabled = !module.remove && module.enabled,
|
enabled = !module.remove && module.enabled,
|
||||||
onClick = { onClick(module) },
|
onClick = { onClick(module) },
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = if (!ThemeConfig.useDynamicColor) {
|
colors = ButtonDefaults.filledTonalButtonColors()
|
||||||
ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.filledTonalButtonColors()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasActionScript && updateUrl.isEmpty()) {
|
//if (!module.hasActionScript && updateUrl.isEmpty()) {
|
||||||
Text(
|
//Text(
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
text = stringResource(R.string.open)
|
// text = stringResource(R.string.open)
|
||||||
)
|
//)
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -844,7 +866,7 @@ fun ModuleItem(
|
|||||||
|
|
||||||
if (updateUrl.isNotEmpty()) {
|
if (updateUrl.isNotEmpty()) {
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
enabled = !module.remove,
|
enabled = !module.remove,
|
||||||
onClick = { onUpdate(module) },
|
onClick = { onUpdate(module) },
|
||||||
shape = ButtonDefaults.textShape,
|
shape = ButtonDefaults.textShape,
|
||||||
@@ -855,30 +877,23 @@ fun ModuleItem(
|
|||||||
imageVector = Icons.Outlined.Download,
|
imageVector = Icons.Outlined.Download,
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
if (!module.hasActionScript || !module.hasWebUi) {
|
//if (!module.hasActionScript || !module.hasWebUi) {
|
||||||
Text(
|
//Text(
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
text = stringResource(R.string.module_update)
|
// text = stringResource(R.string.module_update)
|
||||||
)
|
//)
|
||||||
}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(0.1f, true))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
modifier = Modifier.defaultMinSize(52.dp, 32.dp),
|
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
|
||||||
onClick = { onUninstallClicked(module) },
|
onClick = { onUninstallClicked(module) },
|
||||||
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
contentPadding = ButtonDefaults.TextButtonContentPadding,
|
||||||
colors = if (!ThemeConfig.useDynamicColor) {
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
ButtonDefaults.filledTonalButtonColors(
|
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
|
||||||
containerColor = ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
ButtonDefaults.filledTonalButtonColors()
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
if (!module.remove) {
|
if (!module.remove) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -890,18 +905,18 @@ fun ModuleItem(
|
|||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier.size(20.dp).rotate(180f),
|
modifier = Modifier.size(20.dp).rotate(180f),
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Outlined.Refresh,
|
||||||
contentDescription = null,
|
contentDescription = null
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(start = 7.dp),
|
|
||||||
fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
|
||||||
fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
|
||||||
text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
//if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) {
|
||||||
|
//Text(
|
||||||
|
// modifier = Modifier.padding(start = 7.dp),
|
||||||
|
// fontFamily = MaterialTheme.typography.labelMedium.fontFamily,
|
||||||
|
// fontSize = MaterialTheme.typography.labelMedium.fontSize,
|
||||||
|
// text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore),
|
||||||
|
// color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
//)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -928,4 +943,3 @@ fun ModuleItemPreview() {
|
|||||||
)
|
)
|
||||||
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
|
||||||
}
|
}
|
||||||
|
|
||||||
1376
manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt
Normal file
1376
manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt
Normal file
File diff suppressed because it is too large
Load Diff
769
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
769
manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt
Normal file
@@ -0,0 +1,769 @@
|
|||||||
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.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
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import com.maxkeppeker.sheets.core.models.base.IconSource
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListOption
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import com.sukisu.ultra.BuildConfig
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.*
|
||||||
|
import com.sukisu.ultra.ui.component.*
|
||||||
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
|
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopBar(
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
|
) { paddingValues ->
|
||||||
|
val aboutDialog = rememberCustomDialog {
|
||||||
|
AboutDialog(it)
|
||||||
|
}
|
||||||
|
val loadingDialog = rememberLoadingDialog()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(paddingValues)
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/gzip")
|
||||||
|
) { uri: Uri? ->
|
||||||
|
if (uri == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
loadingDialog.show()
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { output ->
|
||||||
|
getBugreportFile(context).inputStream().use {
|
||||||
|
it.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadingDialog.hide()
|
||||||
|
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.configuration),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 配置文件模板入口
|
||||||
|
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
||||||
|
KsuIsValid {
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Fence,
|
||||||
|
title = profileTemplate,
|
||||||
|
summary = stringResource(id = R.string.settings_profile_template_summary),
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载模块开关
|
||||||
|
var umountChecked by rememberSaveable {
|
||||||
|
mutableStateOf(Natives.isDefaultUmountModules())
|
||||||
|
}
|
||||||
|
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.FolderDelete,
|
||||||
|
title = stringResource(id = R.string.settings_umount_modules_default),
|
||||||
|
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
||||||
|
checked = umountChecked,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (Natives.setDefaultUmountModules(it)) {
|
||||||
|
umountChecked = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SU 禁用开关(仅在兼容版本显示)
|
||||||
|
KsuIsValid {
|
||||||
|
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||||
|
var isSuDisabled by rememberSaveable {
|
||||||
|
mutableStateOf(!Natives.isSuEnabled())
|
||||||
|
}
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.RemoveModerator,
|
||||||
|
title = stringResource(id = R.string.settings_disable_su),
|
||||||
|
summary = stringResource(id = R.string.settings_disable_su_summary),
|
||||||
|
checked = isSuDisabled,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
val shouldEnable = !checked
|
||||||
|
if (Natives.setSuEnabled(shouldEnable)) {
|
||||||
|
isSuDisabled = !shouldEnable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用设置
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_settings),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
// 更新检查开关
|
||||||
|
var checkUpdate by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("check_update", true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.Update,
|
||||||
|
title = stringResource(id = R.string.settings_check_update),
|
||||||
|
summary = stringResource(id = R.string.settings_check_update_summary),
|
||||||
|
checked = checkUpdate,
|
||||||
|
onCheckedChange = {
|
||||||
|
prefs.edit {putBoolean("check_update", it) }
|
||||||
|
checkUpdate = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Web调试开关
|
||||||
|
var enableWebDebugging by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("enable_web_debugging", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchSettingItem(
|
||||||
|
icon = Icons.Filled.DeveloperMode,
|
||||||
|
title = stringResource(id = R.string.enable_web_debugging),
|
||||||
|
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
||||||
|
checked = enableWebDebugging,
|
||||||
|
onCheckedChange = {
|
||||||
|
prefs.edit { putBoolean("enable_web_debugging", it) }
|
||||||
|
enableWebDebugging = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web X 开关
|
||||||
|
var useWebUIX by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("use_webuix", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KsuIsValid {
|
||||||
|
SwitchItem(
|
||||||
|
beta = true,
|
||||||
|
enabled = Platform.isAlive,
|
||||||
|
icon = Icons.Filled.WebAsset,
|
||||||
|
title = stringResource(id = R.string.use_webuix),
|
||||||
|
summary = stringResource(id = R.string.use_webuix_summary),
|
||||||
|
checked = useWebUIX
|
||||||
|
) {
|
||||||
|
prefs.edit { putBoolean("use_webuix", it) }
|
||||||
|
useWebUIX = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web X Eruda 开关
|
||||||
|
var useWebUIXEruda by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getBoolean("use_webuix_eruda", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
KsuIsValid {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = useWebUIX && enableWebDebugging,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
SwitchItem(
|
||||||
|
beta = true,
|
||||||
|
enabled = Platform.isAlive && useWebUIX && enableWebDebugging,
|
||||||
|
icon = Icons.Filled.FormatListNumbered,
|
||||||
|
title = stringResource(id = R.string.use_webuix_eruda),
|
||||||
|
summary = stringResource(id = R.string.use_webuix_eruda_summary),
|
||||||
|
checked = useWebUIXEruda
|
||||||
|
) {
|
||||||
|
prefs.edit { putBoolean("use_webuix_eruda", it) }
|
||||||
|
useWebUIXEruda = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更多设置
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Settings,
|
||||||
|
title = stringResource(id = R.string.more_settings),
|
||||||
|
summary = stringResource(id = R.string.more_settings),
|
||||||
|
onClick = {
|
||||||
|
navigator.navigate(MoreSettingsScreenDestination)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tools),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
var showBottomsheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.BugReport,
|
||||||
|
title = stringResource(id = R.string.send_log),
|
||||||
|
onClick = {
|
||||||
|
showBottomsheet = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showBottomsheet) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = { showBottomsheet = false },
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
LogActionButton(
|
||||||
|
icon = Icons.Filled.Save,
|
||||||
|
text = stringResource(R.string.save_log),
|
||||||
|
onClick = {
|
||||||
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||||
|
val current = LocalDateTime.now().format(formatter)
|
||||||
|
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||||
|
showBottomsheet = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
LogActionButton(
|
||||||
|
icon = Icons.Filled.Share,
|
||||||
|
text = stringResource(R.string.send_log),
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val bugreport = loadingDialog.withLoading {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
getBugreportFile(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val uri: Uri =
|
||||||
|
FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
|
bugreport
|
||||||
|
)
|
||||||
|
|
||||||
|
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
setDataAndType(uri, "application/gzip")
|
||||||
|
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.startActivity(
|
||||||
|
Intent.createChooser(
|
||||||
|
shareIntent,
|
||||||
|
context.getString(R.string.send_log)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
showBottomsheet = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
||||||
|
if (lkmMode) {
|
||||||
|
UninstallItem(navigator) {
|
||||||
|
loadingDialog.withLoading(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置分组卡片 - 关于
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.about),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Info,
|
||||||
|
title = stringResource(R.string.about),
|
||||||
|
onClick = {
|
||||||
|
aboutDialog.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LogActionButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(56.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = text,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
if (summary != null) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchSettingItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!checked) }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
if (summary != null) {
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UninstallItem(
|
||||||
|
navigator: DestinationsNavigator,
|
||||||
|
withLoading: suspend (suspend () -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val uninstallConfirmDialog = rememberConfirmDialog()
|
||||||
|
val showTodo = {
|
||||||
|
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
||||||
|
scope.launch {
|
||||||
|
val result = uninstallConfirmDialog.awaitConfirm(
|
||||||
|
title = context.getString(uninstallType.title),
|
||||||
|
content = context.getString(uninstallType.message)
|
||||||
|
)
|
||||||
|
if (result == ConfirmResult.Confirmed) {
|
||||||
|
withLoading {
|
||||||
|
when (uninstallType) {
|
||||||
|
UninstallType.TEMPORARY -> showTodo()
|
||||||
|
UninstallType.PERMANENT -> navigator.navigate(
|
||||||
|
FlashScreenDestination(FlashIt.FlashUninstall)
|
||||||
|
)
|
||||||
|
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
||||||
|
FlashScreenDestination(FlashIt.FlashRestore)
|
||||||
|
)
|
||||||
|
UninstallType.NONE -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Delete,
|
||||||
|
title = stringResource(id = R.string.settings_uninstall),
|
||||||
|
onClick = {
|
||||||
|
uninstallDialog.show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
||||||
|
TEMPORARY(
|
||||||
|
R.string.settings_uninstall_temporary,
|
||||||
|
R.string.settings_uninstall_temporary_message,
|
||||||
|
Icons.Filled.Delete
|
||||||
|
),
|
||||||
|
PERMANENT(
|
||||||
|
R.string.settings_uninstall_permanent,
|
||||||
|
R.string.settings_uninstall_permanent_message,
|
||||||
|
Icons.Filled.DeleteForever
|
||||||
|
),
|
||||||
|
RESTORE_STOCK_IMAGE(
|
||||||
|
R.string.settings_restore_stock_image,
|
||||||
|
R.string.settings_restore_stock_image_message,
|
||||||
|
Icons.AutoMirrored.Filled.Undo
|
||||||
|
),
|
||||||
|
NONE(0, 0, Icons.Filled.Delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
||||||
|
return rememberCustomDialog { dismiss ->
|
||||||
|
val options = listOf(
|
||||||
|
// UninstallType.TEMPORARY,
|
||||||
|
UninstallType.PERMANENT,
|
||||||
|
UninstallType.RESTORE_STOCK_IMAGE
|
||||||
|
)
|
||||||
|
val listOptions = options.map {
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(it.title),
|
||||||
|
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
||||||
|
icon = IconSource(it.icon)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection = UninstallType.NONE
|
||||||
|
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
||||||
|
ThemeConfig.currentTheme.ButtonContrast
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_uninstall),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
listOptions.forEachIndexed { index, option ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.clickable {
|
||||||
|
selection = options[index]
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = options[index].icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = option.titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
option.subtitleText?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (selection != UninstallType.NONE) {
|
||||||
|
onSelected(selection)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.ok),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
containerColor = cardColor,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
tonalElevation = 4.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) {
|
||||||
|
cardAlpha
|
||||||
|
} else {
|
||||||
|
if (systemIsDark) 0.8f else 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,878 @@
|
|||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import com.sukisu.ultra.Natives
|
||||||
|
import com.sukisu.ultra.ui.component.SearchAppBar
|
||||||
|
import com.sukisu.ultra.ui.util.ModuleModify
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
||||||
|
@Destination<RootGraph>
|
||||||
|
@Composable
|
||||||
|
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
||||||
|
val viewModel = viewModel<SuperUserViewModel>()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
// 添加备份和还原启动器
|
||||||
|
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
||||||
|
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = navigator) {
|
||||||
|
viewModel.search = ""
|
||||||
|
if (viewModel.appList.isEmpty()) {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel.search) {
|
||||||
|
if (viewModel.search.isEmpty()) {
|
||||||
|
// 取消自动滚动到顶部的行为
|
||||||
|
// listState.scrollToItem(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
SearchAppBar(
|
||||||
|
title = { Text(stringResource(R.string.superuser)) },
|
||||||
|
searchText = viewModel.search,
|
||||||
|
onSearchTextChange = { viewModel.search = it },
|
||||||
|
onClearClick = { viewModel.search = "" },
|
||||||
|
dropdownContent = {
|
||||||
|
var showDropdown by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { showDropdown = true },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.MoreVert,
|
||||||
|
contentDescription = stringResource(id = R.string.settings),
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
||||||
|
showDropdown = false
|
||||||
|
}) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.refresh)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Refresh,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.fetchAppList()
|
||||||
|
}
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (viewModel.showSystemApps) {
|
||||||
|
stringResource(R.string.hide_system_apps)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.show_system_apps)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (viewModel.showSystemApps)
|
||||||
|
Icons.Filled.VisibilityOff else Icons.Filled.Visibility,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
viewModel.showSystemApps = !viewModel.showSystemApps
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp))
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.backup_allowlist)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Save,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.restore_allowlist)) },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.RestoreFromTrash,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
||||||
|
showDropdown = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackBarHostState) },
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
floatingActionButton = {
|
||||||
|
// 侧边悬浮按钮集合
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
// 批量操作相关按钮
|
||||||
|
// 只有在批量模式且有选中应用时才显示批量操作按钮
|
||||||
|
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||||
|
// 取消按钮
|
||||||
|
val cancelInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isCancelPressed by cancelInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
viewModel.selectedApps = emptySet()
|
||||||
|
viewModel.showBatchActions = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isCancelPressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = cancelInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(android.R.string.cancel),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCancelPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(android.R.string.cancel),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消授权按钮
|
||||||
|
val unauthorizeInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUnauthorizePressed by unauthorizeInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(false)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isUnauthorizePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = unauthorizeInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Block,
|
||||||
|
contentDescription = stringResource(R.string.batch_cancel_authorization),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isUnauthorizePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.batch_cancel_authorization),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 授权按钮
|
||||||
|
val authorizeInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isAuthorizePressed by authorizeInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isAuthorizePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = authorizeInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Check,
|
||||||
|
contentDescription = stringResource(R.string.batch_authorization),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isAuthorizePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.batch_authorization),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加分隔
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
||||||
|
|
||||||
|
// 在批量操作按钮组中添加卸载模块的按钮
|
||||||
|
// 卸载模块启用按钮
|
||||||
|
val umountEnableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUmountEnablePressed by umountEnableInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(
|
||||||
|
allowSu = false, // 不改变ROOT权限状态
|
||||||
|
umountModules = true // 启用卸载模块
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isUmountEnablePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = umountEnableInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.FolderOff,
|
||||||
|
contentDescription = stringResource(R.string.profile_umount_modules),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isUmountEnablePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.profile_umount_modules),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载模块禁用按钮
|
||||||
|
val umountDisableInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isUmountDisablePressed by umountDisableInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
viewModel.updateBatchPermissions(
|
||||||
|
allowSu = false, // 不改变ROOT权限状态
|
||||||
|
umountModules = false // 禁用卸载模块
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isUmountDisablePressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.tertiaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onTertiaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = umountDisableInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Folder,
|
||||||
|
contentDescription = stringResource(R.string.profile_umount_modules_disable),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isUmountDisablePressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.profile_umount_modules_disable),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 添加分隔
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向上导航按钮
|
||||||
|
val topBtnInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isTopBtnPressed by topBtnInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
listState.animateScrollToItem(0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isTopBtnPressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = topBtnInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.KeyboardArrowUp,
|
||||||
|
contentDescription = stringResource(R.string.scroll_to_top_description),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isTopBtnPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.scroll_to_top),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 向下导航按钮
|
||||||
|
val bottomBtnInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isBottomBtnPressed by bottomBtnInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
val lastIndex = viewModel.appList.size - 1
|
||||||
|
if (lastIndex >= 0) {
|
||||||
|
listState.animateScrollToItem(lastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(if (isBottomBtnPressed) 56.dp else 40.dp),
|
||||||
|
containerColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 1f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
shape = CircleShape,
|
||||||
|
interactionSource = bottomBtnInteractionSource,
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(4.dp, 6.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.KeyboardArrowDown,
|
||||||
|
contentDescription = stringResource(R.string.scroll_to_bottom_description),
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isBottomBtnPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.scroll_to_bottom),
|
||||||
|
modifier = Modifier.padding(end = 4.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { innerPadding ->
|
||||||
|
PullToRefreshBox(
|
||||||
|
modifier = Modifier.padding(innerPadding),
|
||||||
|
onRefresh = {
|
||||||
|
scope.launch { viewModel.fetchAppList() }
|
||||||
|
},
|
||||||
|
isRefreshing = viewModel.isRefreshing
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 8.dp,
|
||||||
|
bottom = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 获取分组后的应用列表
|
||||||
|
val rootApps = viewModel.appList.filter { it.allowSu }
|
||||||
|
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
||||||
|
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
||||||
|
|
||||||
|
// 显示ROOT权限应用组
|
||||||
|
if (rootApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.apps_with_root))
|
||||||
|
}
|
||||||
|
|
||||||
|
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
||||||
|
AppItem(
|
||||||
|
app = app,
|
||||||
|
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||||
|
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||||
|
onSwitchChange = { allowSu ->
|
||||||
|
scope.launch {
|
||||||
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
// 不重新获取应用列表,避免滚动位置重置
|
||||||
|
// viewModel.fetchAppList()
|
||||||
|
// 仅更新当前应用的配置
|
||||||
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
} else {
|
||||||
|
navigator.navigate(AppProfileScreenDestination(app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
// 长按进入多选模式
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleBatchMode()
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示自定义配置应用组
|
||||||
|
if (customApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.apps_with_custom_profile))
|
||||||
|
}
|
||||||
|
|
||||||
|
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
||||||
|
AppItem(
|
||||||
|
app = app,
|
||||||
|
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||||
|
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||||
|
onSwitchChange = { allowSu ->
|
||||||
|
scope.launch {
|
||||||
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
// 不重新获取应用列表,避免滚动位置重置
|
||||||
|
// viewModel.fetchAppList()
|
||||||
|
// 仅更新当前应用的配置
|
||||||
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
} else {
|
||||||
|
navigator.navigate(AppProfileScreenDestination(app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
// 长按进入多选模式
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleBatchMode()
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示其他应用组
|
||||||
|
if (otherApps.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupHeader(title = stringResource(R.string.other_apps))
|
||||||
|
}
|
||||||
|
|
||||||
|
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
||||||
|
AppItem(
|
||||||
|
app = app,
|
||||||
|
isSelected = viewModel.selectedApps.contains(app.packageName),
|
||||||
|
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
||||||
|
onSwitchChange = { allowSu ->
|
||||||
|
scope.launch {
|
||||||
|
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
||||||
|
val updatedProfile = profile.copy(allowSu = allowSu)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
// 不重新获取应用列表,避免滚动位置重置
|
||||||
|
// viewModel.fetchAppList()
|
||||||
|
// 仅更新当前应用的配置
|
||||||
|
viewModel.updateAppProfileLocally(app.packageName, updatedProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
if (viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
} else {
|
||||||
|
navigator.navigate(AppProfileScreenDestination(app))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
// 长按进入多选模式
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
viewModel.toggleBatchMode()
|
||||||
|
viewModel.toggleAppSelection(app.packageName)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当没有应用显示时显示空状态
|
||||||
|
if (viewModel.appList.isEmpty()) {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(400.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Apps,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(96.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.no_apps_found),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupHeader(title: String) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun AppItem(
|
||||||
|
app: SuperUserViewModel.AppInfo,
|
||||||
|
isSelected: Boolean,
|
||||||
|
onToggleSelection: () -> Unit,
|
||||||
|
onSwitchChange: (Boolean) -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
viewModel: SuperUserViewModel
|
||||||
|
) {
|
||||||
|
val cardColor = if (app.allowSu)
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
else if (app.hasCustomProfile)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = cardColor),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.shadow(
|
||||||
|
elevation = 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (isSelected)
|
||||||
|
Modifier.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = MaterialTheme.shapes.medium
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Modifier
|
||||||
|
)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onLongPress = { onLongClick() },
|
||||||
|
onTap = { onClick() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(LocalContext.current)
|
||||||
|
.data(app.packageInfo)
|
||||||
|
.crossfade(true)
|
||||||
|
.build(),
|
||||||
|
contentDescription = app.label,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(48.dp)
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = app.label,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = app.packageName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
if (app.allowSu) {
|
||||||
|
LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary)
|
||||||
|
}
|
||||||
|
if (Natives.uidShouldUmount(app.uid)) {
|
||||||
|
LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary)
|
||||||
|
}
|
||||||
|
if (app.hasCustomProfile) {
|
||||||
|
LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!viewModel.showBatchActions) {
|
||||||
|
// 开关交互源
|
||||||
|
val switchInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isSwitchPressed by switchInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isSwitchPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (app.allowSu) stringResource(R.string.authorized) else stringResource(R.string.unauthorized),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = if (app.allowSu) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Switch(
|
||||||
|
checked = app.allowSu,
|
||||||
|
onCheckedChange = onSwitchChange,
|
||||||
|
interactionSource = switchInteractionSource,
|
||||||
|
colors = SwitchDefaults.colors(
|
||||||
|
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||||
|
checkedIconColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||||
|
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 复选框交互源
|
||||||
|
val checkboxInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState()
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isCheckboxPressed,
|
||||||
|
enter = expandHorizontally() + fadeIn(),
|
||||||
|
exit = shrinkHorizontally() + fadeOut()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (isSelected) stringResource(R.string.selected) else stringResource(R.string.select),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = { onToggleSelection() },
|
||||||
|
interactionSource = checkboxInteractionSource,
|
||||||
|
colors = CheckboxDefaults.colors(
|
||||||
|
checkedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
uncheckedColor = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LabelText(label: String, backgroundColor: Color) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 2.dp, end = 2.dp)
|
||||||
|
.background(
|
||||||
|
backgroundColor,
|
||||||
|
shape = RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp),
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = Color.White,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import LabelText
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,6 +34,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
@@ -44,11 +48,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
@@ -59,8 +62,9 @@ import com.ramcosta.composedestinations.result.ResultRecipient
|
|||||||
import com.ramcosta.composedestinations.result.getOr
|
import com.ramcosta.composedestinations.result.getOr
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.TemplateViewModel
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -77,7 +81,6 @@ fun AppProfileTemplateScreen(
|
|||||||
val viewModel = viewModel<TemplateViewModel>()
|
val viewModel = viewModel<TemplateViewModel>()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.templateList.isEmpty()) {
|
if (viewModel.templateList.isEmpty()) {
|
||||||
@@ -92,10 +95,13 @@ fun AppProfileTemplateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val clipboardManager = context.getSystemService<ClipboardManager>()
|
||||||
val showToast = fun(msg: String) {
|
val showToast = fun(msg: String) {
|
||||||
scope.launch(Dispatchers.Main) {
|
scope.launch(Dispatchers.Main) {
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
||||||
@@ -103,24 +109,28 @@ fun AppProfileTemplateScreen(
|
|||||||
}
|
}
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColorUse.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
onSync = {
|
onSync = {
|
||||||
scope.launch { viewModel.fetchTemplates(true) }
|
scope.launch { viewModel.fetchTemplates(true) }
|
||||||
},
|
},
|
||||||
onImport = {
|
onImport = {
|
||||||
clipboardManager.getText()?.text?.let {
|
scope.launch {
|
||||||
if (it.isEmpty()) {
|
val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString()
|
||||||
|
if (clipboardText.isNullOrEmpty()) {
|
||||||
showToast(context.getString(R.string.app_profile_template_import_empty))
|
showToast(context.getString(R.string.app_profile_template_import_empty))
|
||||||
return@let
|
return@launch
|
||||||
}
|
|
||||||
scope.launch {
|
|
||||||
viewModel.importTemplates(
|
|
||||||
it, {
|
|
||||||
showToast(context.getString(R.string.app_profile_template_import_success))
|
|
||||||
viewModel.fetchTemplates(false)
|
|
||||||
},
|
|
||||||
showToast
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
viewModel.importTemplates(
|
||||||
|
clipboardText,
|
||||||
|
{
|
||||||
|
showToast(context.getString(R.string.app_profile_template_import_success))
|
||||||
|
viewModel.fetchTemplates(false)
|
||||||
|
},
|
||||||
|
showToast
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onExport = {
|
onExport = {
|
||||||
@@ -129,8 +139,8 @@ fun AppProfileTemplateScreen(
|
|||||||
{
|
{
|
||||||
showToast(context.getString(R.string.app_profile_template_export_empty))
|
showToast(context.getString(R.string.app_profile_template_export_empty))
|
||||||
}
|
}
|
||||||
) {
|
) { text ->
|
||||||
clipboardManager.setText(AnnotatedString(it))
|
clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -149,7 +159,6 @@ fun AppProfileTemplateScreen(
|
|||||||
},
|
},
|
||||||
icon = { Icon(Icons.Filled.Add, null) },
|
icon = { Icon(Icons.Filled.Add, null) },
|
||||||
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
text = { Text(stringResource(id = R.string.app_profile_template_create)) },
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -199,17 +208,17 @@ private fun TemplateItem(
|
|||||||
)
|
)
|
||||||
Text(template.description)
|
Text(template.description)
|
||||||
FlowRow {
|
FlowRow {
|
||||||
LabelText(label = "UID: ${template.uid}")
|
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
LabelText(label = "GID: ${template.gid}")
|
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
LabelText(label = template.context)
|
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
if (template.local) {
|
if (template.local) {
|
||||||
LabelText(label = "local")
|
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
} else {
|
} else {
|
||||||
LabelText(label = "remote")
|
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,12 +229,20 @@ private fun TopBar(
|
|||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
onImport: () -> Unit = {},
|
onImport: () -> Unit = {},
|
||||||
onExport: () -> Unit = {},
|
onExport: () -> Unit = {},
|
||||||
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
|
val cardColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(R.string.settings_profile_template))
|
Text(stringResource(R.string.settings_profile_template))
|
||||||
},
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onBack
|
onClick = onBack
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
@@ -47,14 +47,14 @@ import androidx.compose.ui.text.input.KeyboardType
|
|||||||
import com.ramcosta.composedestinations.annotation.Destination
|
import com.ramcosta.composedestinations.annotation.Destination
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
import com.ramcosta.composedestinations.annotation.RootGraph
|
||||||
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
import com.ramcosta.composedestinations.result.ResultBackNavigator
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.RootProfileConfig
|
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||||
import shirkneko.zako.sukisu.ui.util.deleteAppProfileTemplate
|
import com.sukisu.ultra.ui.util.deleteAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.util.getAppProfileTemplate
|
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.util.setAppProfileTemplate
|
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.TemplateViewModel
|
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.toJSON
|
import com.sukisu.ultra.ui.viewmodel.toJSON
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
import androidx.lifecycle.compose.dropUnlessResumed
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.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 mutableFloatStateOf(1f)
|
||||||
|
// 卡片亮度
|
||||||
|
var cardDim by mutableFloatStateOf(0f)
|
||||||
|
// 卡片阴影
|
||||||
|
var cardElevation by mutableStateOf(settingElevation)
|
||||||
|
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)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存卡片配置到SharedPreferences
|
||||||
|
*/
|
||||||
|
fun save(context: Context) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从SharedPreferences加载卡片配置
|
||||||
|
*/
|
||||||
|
fun load(context: Context) {
|
||||||
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||||
|
cardDim = prefs.getFloat("card_dim", 0f)
|
||||||
|
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
|
||||||
|
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
|
||||||
|
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
||||||
|
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
|
||||||
|
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||||
|
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||||
|
updateShadowEnabled(isShadowEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新阴影启用状态
|
||||||
|
*/
|
||||||
|
fun updateShadowEnabled(enabled: Boolean) {
|
||||||
|
isShadowEnabled = enabled
|
||||||
|
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) {
|
||||||
|
customBackgroundElevation
|
||||||
|
} else if (enabled) {
|
||||||
|
settingElevation
|
||||||
|
} else {
|
||||||
|
customBackgroundElevation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置深色模式默认值
|
||||||
|
*/
|
||||||
|
fun setDarkModeDefaults() {
|
||||||
|
if (!isCustomAlphaSet) {
|
||||||
|
cardAlpha = 0.70f
|
||||||
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = 0.5f
|
||||||
|
}
|
||||||
|
updateShadowEnabled(isShadowEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置浅色模式默认值
|
||||||
|
*/
|
||||||
|
fun setLightModeDefaults() {
|
||||||
|
if (!isCustomAlphaSet) {
|
||||||
|
cardAlpha = 1f
|
||||||
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = 0f
|
||||||
|
}
|
||||||
|
updateShadowEnabled(isShadowEnabled)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取卡片颜色配置
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||||
|
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||||
|
contentColor = determineContentColor(originalColor)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun determineContentColor(originalColor: Color): Color {
|
||||||
|
val isDarkTheme = isSystemInDarkTheme()
|
||||||
|
if (ThemeConfig.isThemeChanging) {
|
||||||
|
return if (isDarkTheme) Color.White else Color.Black
|
||||||
|
}
|
||||||
|
|
||||||
|
return when {
|
||||||
|
CardConfig.isUserLightModeEnabled -> Color.Black
|
||||||
|
!isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black
|
||||||
|
isDarkTheme -> Color.White
|
||||||
|
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
|
||||||
|
}
|
||||||
|
}
|
||||||
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
273
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
sealed class ThemeColors {
|
||||||
|
abstract val Primary: Color
|
||||||
|
abstract val Secondary: Color
|
||||||
|
abstract val Tertiary: Color
|
||||||
|
abstract val OnPrimary: Color
|
||||||
|
abstract val OnSecondary: Color
|
||||||
|
abstract val OnTertiary: Color
|
||||||
|
abstract val PrimaryContainer: Color
|
||||||
|
abstract val SecondaryContainer: Color
|
||||||
|
abstract val TertiaryContainer: Color
|
||||||
|
abstract val OnPrimaryContainer: Color
|
||||||
|
abstract val OnSecondaryContainer: Color
|
||||||
|
abstract val OnTertiaryContainer: Color
|
||||||
|
abstract val ButtonContrast: Color
|
||||||
|
|
||||||
|
// 表面颜色
|
||||||
|
abstract val Surface: Color
|
||||||
|
abstract val SurfaceVariant: Color
|
||||||
|
abstract val OnSurface: Color
|
||||||
|
abstract val OnSurfaceVariant: Color
|
||||||
|
|
||||||
|
// 错误状态颜色
|
||||||
|
abstract val Error: Color
|
||||||
|
abstract val OnError: Color
|
||||||
|
abstract val ErrorContainer: Color
|
||||||
|
abstract val OnErrorContainer: Color
|
||||||
|
|
||||||
|
// 边框和背景色
|
||||||
|
abstract val Outline: Color
|
||||||
|
abstract val OutlineVariant: Color
|
||||||
|
abstract val Background: Color
|
||||||
|
abstract val OnBackground: Color
|
||||||
|
|
||||||
|
// 默认主题 (蓝色)
|
||||||
|
object Default : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF2196F3)
|
||||||
|
override val Secondary = Color(0xFF64B5F6)
|
||||||
|
override val Tertiary = Color(0xFF0D47A1)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFD6EAFF)
|
||||||
|
override val SecondaryContainer = Color(0xFFE3F2FD)
|
||||||
|
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF0A3049)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF0D3C61)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF071D41)
|
||||||
|
override val ButtonContrast = Color(0xFF2196F3)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFF5F9FF)
|
||||||
|
override val SurfaceVariant = Color(0xFFEDF5FE)
|
||||||
|
override val OnSurface = Color(0xFF1A1C1E)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF42474E)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFB00020)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFDE7E9)
|
||||||
|
override val OnErrorContainer = Color(0xFF410008)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFBAC3CF)
|
||||||
|
override val OutlineVariant = Color(0xFFDFE3EB)
|
||||||
|
override val Background = Color(0xFFFAFCFF)
|
||||||
|
override val OnBackground = Color(0xFF1A1C1E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绿色主题
|
||||||
|
object Green : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF43A047)
|
||||||
|
override val Secondary = Color(0xFF66BB6A)
|
||||||
|
override val Tertiary = Color(0xFF1B5E20)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFD8EFDB)
|
||||||
|
override val SecondaryContainer = Color(0xFFE8F5E9)
|
||||||
|
override val TertiaryContainer = Color(0xFFB9F6CA)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF0A280D)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF0E2912)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF051B07)
|
||||||
|
override val ButtonContrast = Color(0xFF43A047)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFF6FBF6)
|
||||||
|
override val SurfaceVariant = Color(0xFFEDF7EE)
|
||||||
|
override val OnSurface = Color(0xFF191C19)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF414941)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFC62828)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFF8D7DA)
|
||||||
|
override val OnErrorContainer = Color(0xFF4A0808)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFBDC9BF)
|
||||||
|
override val OutlineVariant = Color(0xFFDDE6DE)
|
||||||
|
override val Background = Color(0xFFFBFDFB)
|
||||||
|
override val OnBackground = Color(0xFF191C19)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 紫色主题
|
||||||
|
object Purple : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF9C27B0)
|
||||||
|
override val Secondary = Color(0xFFBA68C8)
|
||||||
|
override val Tertiary = Color(0xFF6A1B9A)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFF3D8F8)
|
||||||
|
override val SecondaryContainer = Color(0xFFF5E9F7)
|
||||||
|
override val TertiaryContainer = Color(0xFFE1BEE7)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF2A0934)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF3C0F50)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF1D0830)
|
||||||
|
override val ButtonContrast = Color(0xFF9C27B0)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFCF6FF)
|
||||||
|
override val SurfaceVariant = Color(0xFFF5EEFA)
|
||||||
|
override val OnSurface = Color(0xFF1D1B1E)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF49454E)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFD50000)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDCD5)
|
||||||
|
override val OnErrorContainer = Color(0xFF480000)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFC9B9D0)
|
||||||
|
override val OutlineVariant = Color(0xFFE8DAED)
|
||||||
|
override val Background = Color(0xFFFFFBFF)
|
||||||
|
override val OnBackground = Color(0xFF1D1B1E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 橙色主题
|
||||||
|
object Orange : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFFFF9800)
|
||||||
|
override val Secondary = Color(0xFFFFB74D)
|
||||||
|
override val Tertiary = Color(0xFFE65100)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFF000000)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFFFECCC)
|
||||||
|
override val SecondaryContainer = Color(0xFFFFF0D9)
|
||||||
|
override val TertiaryContainer = Color(0xFFFFD180)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF351F00)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF3D2800)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF2E1500)
|
||||||
|
override val ButtonContrast = Color(0xFFFF9800)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFFF8F3)
|
||||||
|
override val SurfaceVariant = Color(0xFFFFF0E6)
|
||||||
|
override val OnSurface = Color(0xFF1F1B16)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF4E4639)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFD32F2F)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDBC8)
|
||||||
|
override val OnErrorContainer = Color(0xFF490700)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFD6C3AD)
|
||||||
|
override val OutlineVariant = Color(0xFFEFDFCC)
|
||||||
|
override val Background = Color(0xFFFFFBFF)
|
||||||
|
override val OnBackground = Color(0xFF1F1B16)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 粉色主题
|
||||||
|
object Pink : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFFE91E63)
|
||||||
|
override val Secondary = Color(0xFFF06292)
|
||||||
|
override val Tertiary = Color(0xFF880E4F)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFFCE4EC)
|
||||||
|
override val SecondaryContainer = Color(0xFFFCE4EC)
|
||||||
|
override val TertiaryContainer = Color(0xFFF8BBD0)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF3B0819)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF3B0819)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF2B0516)
|
||||||
|
override val ButtonContrast = Color(0xFFE91E63)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFFF7F9)
|
||||||
|
override val SurfaceVariant = Color(0xFFFCEEF2)
|
||||||
|
override val OnSurface = Color(0xFF201A1C)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF534347)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFB71C1C)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
override val OnErrorContainer = Color(0xFF410002)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFD6BABF)
|
||||||
|
override val OutlineVariant = Color(0xFFEFDDE0)
|
||||||
|
override val Background = Color(0xFFFFFBFF)
|
||||||
|
override val OnBackground = Color(0xFF201A1C)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 灰色主题
|
||||||
|
object Gray : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFF607D8B)
|
||||||
|
override val Secondary = Color(0xFF90A4AE)
|
||||||
|
override val Tertiary = Color(0xFF455A64)
|
||||||
|
override val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
override val OnSecondary = Color(0xFFFFFFFF)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFECEFF1)
|
||||||
|
override val SecondaryContainer = Color(0xFFECEFF1)
|
||||||
|
override val TertiaryContainer = Color(0xFFCFD8DC)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF1A2327)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF1A2327)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF121A1D)
|
||||||
|
override val ButtonContrast = Color(0xFF607D8B)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFF6F9FB)
|
||||||
|
override val SurfaceVariant = Color(0xFFEEF2F4)
|
||||||
|
override val OnSurface = Color(0xFF191C1E)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF41484D)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFC62828)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
override val OnErrorContainer = Color(0xFF410002)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFBDC1C4)
|
||||||
|
override val OutlineVariant = Color(0xFFDDE1E3)
|
||||||
|
override val Background = Color(0xFFFBFCFE)
|
||||||
|
override val OnBackground = Color(0xFF191C1E)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 黄色主题
|
||||||
|
object Yellow : ThemeColors() {
|
||||||
|
override val Primary = Color(0xFFFFC107)
|
||||||
|
override val Secondary = Color(0xFFFFD54F)
|
||||||
|
override val Tertiary = Color(0xFFFF8F00)
|
||||||
|
override val OnPrimary = Color(0xFF000000)
|
||||||
|
override val OnSecondary = Color(0xFF000000)
|
||||||
|
override val OnTertiary = Color(0xFFFFFFFF)
|
||||||
|
override val PrimaryContainer = Color(0xFFFFF8E1)
|
||||||
|
override val SecondaryContainer = Color(0xFFFFF8E1)
|
||||||
|
override val TertiaryContainer = Color(0xFFFFECB3)
|
||||||
|
override val OnPrimaryContainer = Color(0xFF332A00)
|
||||||
|
override val OnSecondaryContainer = Color(0xFF332A00)
|
||||||
|
override val OnTertiaryContainer = Color(0xFF221200)
|
||||||
|
override val ButtonContrast = Color(0xFFFFC107)
|
||||||
|
|
||||||
|
override val Surface = Color(0xFFFFFAF3)
|
||||||
|
override val SurfaceVariant = Color(0xFFFFF7E6)
|
||||||
|
override val OnSurface = Color(0xFF1F1C17)
|
||||||
|
override val OnSurfaceVariant = Color(0xFF4E4A3C)
|
||||||
|
|
||||||
|
override val Error = Color(0xFFB71C1C)
|
||||||
|
override val OnError = Color(0xFFFFFFFF)
|
||||||
|
override val ErrorContainer = Color(0xFFFFDAD6)
|
||||||
|
override val OnErrorContainer = Color(0xFF410002)
|
||||||
|
|
||||||
|
override val Outline = Color(0xFFD1C8AF)
|
||||||
|
override val OutlineVariant = Color(0xFFEEE8D7)
|
||||||
|
override val Background = Color(0xFFFFFCF8)
|
||||||
|
override val OnBackground = Color(0xFF1F1C17)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||||
|
"green" -> Green
|
||||||
|
"purple" -> Purple
|
||||||
|
"orange" -> Orange
|
||||||
|
"pink" -> Pink
|
||||||
|
"gray" -> Gray
|
||||||
|
"yellow" -> Yellow
|
||||||
|
else -> Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
584
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
584
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.paint
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import coil.compose.AsyncImagePainter
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.sukisu.ultra.ui.util.BackgroundTransformation
|
||||||
|
import com.sukisu.ultra.ui.util.saveTransformedBackground
|
||||||
|
import androidx.activity.SystemBarStyle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题配置对象,管理应用的主题相关状态
|
||||||
|
*/
|
||||||
|
object ThemeConfig {
|
||||||
|
var customBackgroundUri by mutableStateOf<Uri?>(null)
|
||||||
|
var forceDarkMode by mutableStateOf<Boolean?>(null)
|
||||||
|
var currentTheme by mutableStateOf<ThemeColors>(ThemeColors.Default)
|
||||||
|
var useDynamicColor by mutableStateOf(false)
|
||||||
|
var backgroundImageLoaded by mutableStateOf(false)
|
||||||
|
var needsResetOnThemeChange by mutableStateOf(false)
|
||||||
|
var isThemeChanging by mutableStateOf(false)
|
||||||
|
var preventBackgroundRefresh by mutableStateOf(false)
|
||||||
|
|
||||||
|
private var lastDarkModeState: Boolean? = null
|
||||||
|
fun detectThemeChange(currentDarkMode: Boolean): Boolean {
|
||||||
|
val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode
|
||||||
|
lastDarkModeState = currentDarkMode
|
||||||
|
return isChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetBackgroundState() {
|
||||||
|
if (!preventBackgroundRefresh) {
|
||||||
|
backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
isThemeChanging = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用主题
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun KernelSUTheme(
|
||||||
|
darkTheme: Boolean = when(ThemeConfig.forceDarkMode) {
|
||||||
|
true -> true
|
||||||
|
false -> false
|
||||||
|
null -> isSystemInDarkTheme()
|
||||||
|
},
|
||||||
|
dynamicColor: Boolean = ThemeConfig.useDynamicColor,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val systemIsDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
// 检测系统主题变化并保存状态
|
||||||
|
val themeChanged = ThemeConfig.detectThemeChange(systemIsDark)
|
||||||
|
LaunchedEffect(systemIsDark, themeChanged) {
|
||||||
|
if (ThemeConfig.forceDarkMode == null && themeChanged) {
|
||||||
|
Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark")
|
||||||
|
ThemeConfig.resetBackgroundState()
|
||||||
|
|
||||||
|
if (!ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
context.loadCustomBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
CardConfig.apply {
|
||||||
|
load(context)
|
||||||
|
if (!isCustomAlphaSet) {
|
||||||
|
cardAlpha = if (systemIsDark) 0.50f else 1f
|
||||||
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = if (systemIsDark) 0.5f else 0f
|
||||||
|
}
|
||||||
|
save(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemBarStyle(
|
||||||
|
darkMode = darkTheme
|
||||||
|
)
|
||||||
|
|
||||||
|
// 初始加载配置
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
context.loadThemeMode()
|
||||||
|
context.loadThemeColors()
|
||||||
|
context.loadDynamicColorState()
|
||||||
|
CardConfig.load(context)
|
||||||
|
|
||||||
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
context.loadCustomBackground()
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("prevent_background_refresh", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建颜色方案
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context)
|
||||||
|
}
|
||||||
|
darkTheme -> createDarkColorScheme()
|
||||||
|
else -> createLightColorScheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据暗色模式和自定义背景调整卡片配置
|
||||||
|
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
|
||||||
|
if (darkTheme && !dynamicColor) {
|
||||||
|
CardConfig.setDarkModeDefaults()
|
||||||
|
} else if (!darkTheme && !dynamicColor) {
|
||||||
|
CardConfig.setLightModeDefaults()
|
||||||
|
}
|
||||||
|
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
|
||||||
|
|
||||||
|
val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||||
|
|
||||||
|
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||||
|
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||||
|
}
|
||||||
|
|
||||||
|
val bgImagePainter = backgroundUri.value?.let {
|
||||||
|
rememberAsyncImagePainter(
|
||||||
|
model = it,
|
||||||
|
onError = {
|
||||||
|
Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}")
|
||||||
|
ThemeConfig.customBackgroundUri = null
|
||||||
|
context.saveCustomBackground(null)
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
Log.d("ThemeSystem", "背景图加载成功")
|
||||||
|
ThemeConfig.backgroundImageLoaded = true
|
||||||
|
ThemeConfig.isThemeChanging = false
|
||||||
|
|
||||||
|
ThemeConfig.preventBackgroundRefresh = true
|
||||||
|
context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit { putBoolean("prevent_background_refresh", true) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val transition = updateTransition(
|
||||||
|
targetState = ThemeConfig.backgroundImageLoaded,
|
||||||
|
label = "bgTransition"
|
||||||
|
)
|
||||||
|
val bgAlpha by transition.animateFloat(
|
||||||
|
label = "bgAlpha",
|
||||||
|
transitionSpec = {
|
||||||
|
spring(
|
||||||
|
dampingRatio = 0.8f,
|
||||||
|
stiffness = 300f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { loaded -> if (loaded) 1f else 0f }
|
||||||
|
|
||||||
|
DisposableEffect(systemIsDark) {
|
||||||
|
onDispose {
|
||||||
|
if (ThemeConfig.isThemeChanging) {
|
||||||
|
ThemeConfig.isThemeChanging = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算适用的暗化值
|
||||||
|
val dimFactor = CardConfig.cardDim
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-2f)
|
||||||
|
.background(if (darkTheme) Color.Black else Color.White)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 自定义背景层
|
||||||
|
backgroundUri.value?.let { uri ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-1f)
|
||||||
|
.alpha(bgAlpha)
|
||||||
|
) {
|
||||||
|
// 背景图片
|
||||||
|
bgImagePainter?.let { painter ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.paint(
|
||||||
|
painter = painter,
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 亮度调节层 (根据cardDim调整)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
|
||||||
|
else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// 边缘渐变遮罩
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
|
||||||
|
else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
|
||||||
|
),
|
||||||
|
radius = 1200f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容层
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(1f)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建动态深色颜色方案
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
|
@Composable
|
||||||
|
private fun createDynamicDarkColorScheme(context: Context) =
|
||||||
|
dynamicDarkColorScheme(context).copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onBackground = Color.White,
|
||||||
|
onSurface = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建动态浅色颜色方案
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
|
@Composable
|
||||||
|
private fun createDynamicLightColorScheme(context: Context) =
|
||||||
|
dynamicLightColorScheme(context).copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建深色颜色方案
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun createDarkColorScheme() = darkColorScheme(
|
||||||
|
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f),
|
||||||
|
onPrimary = Color.White,
|
||||||
|
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f),
|
||||||
|
onPrimaryContainer = Color.White,
|
||||||
|
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f),
|
||||||
|
onSecondary = Color.White,
|
||||||
|
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f),
|
||||||
|
onSecondaryContainer = Color.White,
|
||||||
|
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f),
|
||||||
|
onTertiary = Color.White,
|
||||||
|
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f),
|
||||||
|
onTertiaryContainer = Color.White,
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onBackground = Color.White,
|
||||||
|
onSurface = Color.White,
|
||||||
|
surfaceVariant = Color(0xFF2F2F2F),
|
||||||
|
onSurfaceVariant = Color.White.copy(alpha = 0.7f),
|
||||||
|
outline = Color.White.copy(alpha = 0.12f),
|
||||||
|
outlineVariant = Color.White.copy(alpha = 0.12f),
|
||||||
|
error = ThemeConfig.currentTheme.Error,
|
||||||
|
onError = ThemeConfig.currentTheme.OnError,
|
||||||
|
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f),
|
||||||
|
onErrorContainer = Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建浅色颜色方案
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun createLightColorScheme() = lightColorScheme(
|
||||||
|
primary = ThemeConfig.currentTheme.Primary,
|
||||||
|
onPrimary = ThemeConfig.currentTheme.OnPrimary,
|
||||||
|
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer,
|
||||||
|
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer,
|
||||||
|
secondary = ThemeConfig.currentTheme.Secondary,
|
||||||
|
onSecondary = ThemeConfig.currentTheme.OnSecondary,
|
||||||
|
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer,
|
||||||
|
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer,
|
||||||
|
tertiary = ThemeConfig.currentTheme.Tertiary,
|
||||||
|
onTertiary = ThemeConfig.currentTheme.OnTertiary,
|
||||||
|
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer,
|
||||||
|
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer,
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent,
|
||||||
|
onBackground = Color.Black.copy(alpha = 0.87f),
|
||||||
|
onSurface = Color.Black.copy(alpha = 0.87f),
|
||||||
|
surfaceVariant = Color(0xFFF5F5F5),
|
||||||
|
onSurfaceVariant = Color.Black.copy(alpha = 0.78f),
|
||||||
|
outline = Color.Black.copy(alpha = 0.12f),
|
||||||
|
outlineVariant = Color.Black.copy(alpha = 0.12f),
|
||||||
|
error = ThemeConfig.currentTheme.Error,
|
||||||
|
onError = ThemeConfig.currentTheme.OnError,
|
||||||
|
errorContainer = ThemeConfig.currentTheme.ErrorContainer,
|
||||||
|
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制图片到应用内部存储并提升持久性
|
||||||
|
*/
|
||||||
|
private fun Context.copyImageToInternalStorage(uri: Uri): Uri? {
|
||||||
|
return try {
|
||||||
|
val contentResolver: ContentResolver = contentResolver
|
||||||
|
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
|
||||||
|
val fileName = "custom_background.jpg"
|
||||||
|
val file = File(filesDir, fileName)
|
||||||
|
|
||||||
|
val backupFile = File(filesDir, "${fileName}.backup")
|
||||||
|
val outputStream = FileOutputStream(backupFile)
|
||||||
|
val buffer = ByteArray(4 * 1024)
|
||||||
|
var read: Int
|
||||||
|
|
||||||
|
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||||
|
outputStream.write(buffer, 0, read)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
inputStream.close()
|
||||||
|
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
backupFile.renameTo(file)
|
||||||
|
|
||||||
|
Uri.fromFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("ImageCopy", "复制图片失败: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存并应用自定义背景
|
||||||
|
*/
|
||||||
|
fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) {
|
||||||
|
val finalUri = if (transformation != null) {
|
||||||
|
saveTransformedBackground(uri, transformation)
|
||||||
|
} else {
|
||||||
|
copyImageToInternalStorage(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("custom_background", finalUri?.toString())
|
||||||
|
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.customBackgroundUri = finalUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存自定义背景
|
||||||
|
*/
|
||||||
|
fun Context.saveCustomBackground(uri: Uri?) {
|
||||||
|
val newUri = uri?.let { copyImageToInternalStorage(it) }
|
||||||
|
|
||||||
|
// 保存到配置文件
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("custom_background", newUri?.toString())
|
||||||
|
if (uri == null) {
|
||||||
|
// 如果清除背景,也重置阻止刷新标志
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
} else {
|
||||||
|
// 设置阻止刷新标志为false,允许新设置的背景加载一次
|
||||||
|
putBoolean("prevent_background_refresh", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.customBackgroundUri = newUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
ThemeConfig.preventBackgroundRefresh = false
|
||||||
|
|
||||||
|
if (uri != null) {
|
||||||
|
CardConfig.cardElevation = 0.dp
|
||||||
|
CardConfig.isCustomBackgroundEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载自定义背景
|
||||||
|
*/
|
||||||
|
fun Context.loadCustomBackground() {
|
||||||
|
val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getString("custom_background", null)
|
||||||
|
|
||||||
|
val newUri = uriString?.toUri()
|
||||||
|
val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("prevent_background_refresh", false)
|
||||||
|
|
||||||
|
ThemeConfig.preventBackgroundRefresh = preventRefresh
|
||||||
|
|
||||||
|
if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) {
|
||||||
|
Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh")
|
||||||
|
ThemeConfig.customBackgroundUri = newUri
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题模式
|
||||||
|
*/
|
||||||
|
fun Context.saveThemeMode(forceDark: Boolean?) {
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString(
|
||||||
|
"theme_mode", when (forceDark) {
|
||||||
|
true -> "dark"
|
||||||
|
false -> "light"
|
||||||
|
null -> "system"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ThemeConfig.forceDarkMode = forceDark
|
||||||
|
ThemeConfig.needsResetOnThemeChange = forceDark == null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载主题模式
|
||||||
|
*/
|
||||||
|
fun Context.loadThemeMode() {
|
||||||
|
val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getString("theme_mode", "system")
|
||||||
|
|
||||||
|
ThemeConfig.forceDarkMode = when(mode) {
|
||||||
|
"dark" -> true
|
||||||
|
"light" -> false
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存主题颜色
|
||||||
|
*/
|
||||||
|
fun Context.saveThemeColors(themeName: String) {
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putString("theme_colors", themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
ThemeConfig.currentTheme = ThemeColors.fromName(themeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载主题颜色
|
||||||
|
*/
|
||||||
|
fun Context.loadThemeColors() {
|
||||||
|
val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getString("theme_colors", "default")
|
||||||
|
|
||||||
|
ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存动态颜色状态
|
||||||
|
*/
|
||||||
|
fun Context.saveDynamicColorState(enabled: Boolean) {
|
||||||
|
getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.edit {
|
||||||
|
putBoolean("use_dynamic_color", enabled)
|
||||||
|
}
|
||||||
|
ThemeConfig.useDynamicColor = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载动态颜色状态
|
||||||
|
*/
|
||||||
|
fun Context.loadDynamicColorState() {
|
||||||
|
val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("use_dynamic_color", true)
|
||||||
|
|
||||||
|
ThemeConfig.useDynamicColor = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
108
manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
package com.sukisu.ultra.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
val Typography = Typography(
|
||||||
|
// 大标题
|
||||||
|
displayLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 57.sp,
|
||||||
|
lineHeight = 64.sp,
|
||||||
|
letterSpacing = (-0.25).sp
|
||||||
|
),
|
||||||
|
displayMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 45.sp,
|
||||||
|
lineHeight = 52.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
displaySmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 36.sp,
|
||||||
|
lineHeight = 44.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
headlineLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
lineHeight = 40.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
headlineMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 28.sp,
|
||||||
|
lineHeight = 36.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
headlineSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
lineHeight = 32.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 标题栏
|
||||||
|
titleLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 22.sp,
|
||||||
|
lineHeight = 28.sp,
|
||||||
|
letterSpacing = 0.sp
|
||||||
|
),
|
||||||
|
titleMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.15.sp
|
||||||
|
),
|
||||||
|
titleSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 主体文字
|
||||||
|
bodyLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 16.sp,
|
||||||
|
lineHeight = 24.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
|
bodyMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.25.sp
|
||||||
|
),
|
||||||
|
bodySmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.4.sp
|
||||||
|
),
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
labelLarge = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
lineHeight = 20.sp,
|
||||||
|
letterSpacing = 0.1.sp
|
||||||
|
),
|
||||||
|
labelMedium = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
),
|
||||||
|
labelSmall = TextStyle(
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
lineHeight = 16.sp,
|
||||||
|
letterSpacing = 0.5.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import androidx.core.graphics.createBitmap
|
||||||
|
|
||||||
|
data class BackgroundTransformation(
|
||||||
|
val scale: Float = 1f,
|
||||||
|
val offsetX: Float = 0f,
|
||||||
|
val offsetY: Float = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Context.getImageBitmap(uri: Uri): Bitmap? {
|
||||||
|
return try {
|
||||||
|
val contentResolver: ContentResolver = contentResolver
|
||||||
|
val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null
|
||||||
|
val bitmap = BitmapFactory.decodeStream(inputStream)
|
||||||
|
inputStream.close()
|
||||||
|
bitmap
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap {
|
||||||
|
val width = bitmap.width
|
||||||
|
val height = bitmap.height
|
||||||
|
|
||||||
|
// 创建与屏幕比例相同的目标位图
|
||||||
|
val displayMetrics = resources.displayMetrics
|
||||||
|
val screenWidth = displayMetrics.widthPixels
|
||||||
|
val screenHeight = displayMetrics.heightPixels
|
||||||
|
val screenRatio = screenHeight.toFloat() / screenWidth.toFloat()
|
||||||
|
|
||||||
|
// 计算目标宽高
|
||||||
|
val targetWidth: Int
|
||||||
|
val targetHeight: Int
|
||||||
|
if (width.toFloat() / height.toFloat() > screenRatio) {
|
||||||
|
targetHeight = height
|
||||||
|
targetWidth = (height / screenRatio).toInt()
|
||||||
|
} else {
|
||||||
|
targetWidth = width
|
||||||
|
targetHeight = (width * screenRatio).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建与目标相同大小的位图
|
||||||
|
val scaledBitmap = createBitmap(targetWidth, targetHeight)
|
||||||
|
val canvas = Canvas(scaledBitmap)
|
||||||
|
|
||||||
|
val matrix = Matrix()
|
||||||
|
|
||||||
|
// 确保缩放值有效
|
||||||
|
val safeScale = maxOf(0.1f, transformation.scale)
|
||||||
|
matrix.postScale(safeScale, safeScale)
|
||||||
|
|
||||||
|
// 计算偏移量,确保不会出现负最大值的问题
|
||||||
|
val widthDiff = (bitmap.width * safeScale - targetWidth)
|
||||||
|
val heightDiff = (bitmap.height * safeScale - targetHeight)
|
||||||
|
|
||||||
|
// 安全计算偏移量边界
|
||||||
|
val maxOffsetX = maxOf(0f, widthDiff / 2)
|
||||||
|
val maxOffsetY = maxOf(0f, heightDiff / 2)
|
||||||
|
|
||||||
|
// 限制偏移范围
|
||||||
|
val safeOffsetX = if (maxOffsetX > 0)
|
||||||
|
transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f
|
||||||
|
val safeOffsetY = if (maxOffsetY > 0)
|
||||||
|
transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f
|
||||||
|
|
||||||
|
// 应用偏移量到矩阵
|
||||||
|
val translationX = -widthDiff / 2 + safeOffsetX
|
||||||
|
val translationY = -heightDiff / 2 + safeOffsetY
|
||||||
|
|
||||||
|
matrix.postTranslate(translationX, translationY)
|
||||||
|
|
||||||
|
// 将原始位图绘制到新位图上
|
||||||
|
canvas.drawBitmap(bitmap, matrix, null)
|
||||||
|
|
||||||
|
return scaledBitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? {
|
||||||
|
try {
|
||||||
|
val bitmap = getImageBitmap(uri) ?: return null
|
||||||
|
val transformedBitmap = applyTransformationToBitmap(bitmap, transformation)
|
||||||
|
|
||||||
|
val fileName = "custom_background_transformed.jpg"
|
||||||
|
val file = File(filesDir, fileName)
|
||||||
|
val outputStream = FileOutputStream(file)
|
||||||
|
|
||||||
|
transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
|
||||||
|
outputStream.flush()
|
||||||
|
outputStream.close()
|
||||||
|
|
||||||
|
return Uri.fromFile(file)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
@@ -12,7 +12,8 @@ import android.util.Log
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import shirkneko.zako.sukisu.ui.util.module.LatestVersionInfo
|
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -42,14 +43,14 @@ fun download(
|
|||||||
onDownloading()
|
onDownloading()
|
||||||
return
|
return
|
||||||
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
} else if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
onDownloaded(Uri.parse(localUri))
|
onDownloaded(localUri.toUri())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(url.toUri())
|
||||||
.setDestinationInExternalPublicDir(
|
.setDestinationInExternalPublicDir(
|
||||||
Environment.DIRECTORY_DOWNLOADS,
|
Environment.DIRECTORY_DOWNLOADS,
|
||||||
fileName
|
fileName
|
||||||
@@ -63,7 +64,6 @@ fun download(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun checkNewVersion(): LatestVersionInfo {
|
fun checkNewVersion(): LatestVersionInfo {
|
||||||
// 改为新的 release 接口
|
|
||||||
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
|
||||||
val defaultValue = LatestVersionInfo()
|
val defaultValue = LatestVersionInfo()
|
||||||
return runCatching {
|
return runCatching {
|
||||||
@@ -141,7 +141,7 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
|
|||||||
val uri = cursor.getString(
|
val uri = cursor.getString(
|
||||||
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
|
||||||
)
|
)
|
||||||
onDownloaded(Uri.parse(uri))
|
onDownloaded(uri.toUri())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util;
|
package com.sukisu.ultra.ui.util;
|
||||||
/*
|
/*
|
||||||
* Copyright (C) 2009 The Android Open Source Project
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
*
|
*
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -16,9 +16,9 @@ import com.topjohnwu.superuser.ShellUtils
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ import java.io.File
|
|||||||
private const val TAG = "KsuCli"
|
private const val TAG = "KsuCli"
|
||||||
|
|
||||||
private fun getKsuDaemonPath(): String {
|
private fun getKsuDaemonPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakomk.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
object KsuCli {
|
object KsuCli {
|
||||||
@@ -436,8 +436,8 @@ fun restartApp(packageName: String) {
|
|||||||
launchApp(packageName)
|
launchApp(packageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSuSFSDaemonPath(): String {
|
fun getSuSFSDaemonPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakomksd.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSuSFS(): String {
|
fun getSuSFS(): String {
|
||||||
@@ -481,7 +481,7 @@ fun susfsSUS_SU_Mode(): String {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getKpmmgrPath(): String {
|
fun getKpmmgrPath(): String {
|
||||||
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
|
return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,48 +489,62 @@ private fun getKpmmgrPath(): String {
|
|||||||
fun loadKpmModule(path: String, args: String? = null): String {
|
fun loadKpmModule(path: String, args: String? = null): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
|
val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unloadKpmModule(name: String): String {
|
fun unloadKpmModule(name: String): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} unload $name"
|
val cmd = "${getKpmmgrPath()} unload $name"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKpmModuleCount(): String {
|
fun getKpmModuleCount(): Int {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} num"
|
val cmd = "${getKpmmgrPath()} num"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
return result.trim().toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun runCmd(shell : Shell, cmd : String) : String {
|
||||||
|
return shell.newJob()
|
||||||
|
.add(cmd)
|
||||||
|
.to(mutableListOf<String>(), null)
|
||||||
|
.exec().out
|
||||||
|
.joinToString("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun listKpmModules(): String {
|
fun listKpmModules(): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} list"
|
val cmd = "${getKpmmgrPath()} list"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return try {
|
||||||
return result
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to list KPM modules", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getKpmModuleInfo(name: String): String {
|
fun getKpmModuleInfo(name: String): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} info $name"
|
val cmd = "${getKpmmgrPath()} info $name"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
return try {
|
||||||
return result
|
runCmd(shell, cmd).trim()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get KPM module info: $name", e)
|
||||||
|
""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun controlKpmModule(name: String, args: String? = null): String {
|
fun controlKpmModule(name: String, args: String? = null): Int {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} control $name ${args ?: ""}"
|
val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}""""
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
val result = runCmd(shell, cmd)
|
||||||
return result
|
return result.trim().toIntOrNull() ?: -1
|
||||||
}
|
}
|
||||||
|
|
||||||
fun printKpmModules(): String {
|
fun getKpmVersion(): String {
|
||||||
val shell = getRootShell()
|
val shell = getRootShell()
|
||||||
val cmd = "${getKpmmgrPath()} print"
|
val cmd = "${getKpmmgrPath()} version"
|
||||||
val result = ShellUtils.fastCmd(shell, cmd)
|
val result = ShellUtils.fastCmd(shell, cmd)
|
||||||
return result
|
return result.trim()
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.system.Os
|
import android.system.Os
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.ui.screen.getManagerVersion
|
import com.sukisu.ultra.ui.screen.getManagerVersion
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
@@ -24,7 +24,7 @@ fun getBugreportFile(context: Context): File {
|
|||||||
val pstoreFile = File(bugreportDir, "pstore.tar.gz")
|
val pstoreFile = File(bugreportDir, "pstore.tar.gz")
|
||||||
// Xiaomi/Readmi devices have diag in /data/vendor/diag
|
// Xiaomi/Readmi devices have diag in /data/vendor/diag
|
||||||
val diagFile = File(bugreportDir, "diag.tar.gz")
|
val diagFile = File(bugreportDir, "diag.tar.gz")
|
||||||
val opulsFile = File(bugreportDir, "opuls.tar.gz")
|
val oplusFile = File(bugreportDir, "oplus.tar.gz")
|
||||||
val bootlogFile = File(bugreportDir, "bootlog.tar.gz")
|
val bootlogFile = File(bugreportDir, "bootlog.tar.gz")
|
||||||
val mountsFile = File(bugreportDir, "mounts.txt")
|
val mountsFile = File(bugreportDir, "mounts.txt")
|
||||||
val fileSystemsFile = File(bugreportDir, "filesystems.txt")
|
val fileSystemsFile = File(bugreportDir, "filesystems.txt")
|
||||||
@@ -46,7 +46,7 @@ fun getBugreportFile(context: Context): File {
|
|||||||
shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
|
shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec()
|
||||||
shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
|
shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec()
|
||||||
shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
|
shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec()
|
||||||
shell.newJob().add("tar -czf ${opulsFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
|
shell.newJob().add("tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec()
|
||||||
shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
|
shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec()
|
||||||
|
|
||||||
shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
|
shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -16,7 +16,7 @@ import kotlinx.coroutines.CompletableDeferred
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util
|
package com.sukisu.ultra.ui.util
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import shirkneko.zako.sukisu.R
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun getSELinuxStatus(): String {
|
fun getSELinuxStatus(): String {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.util.module
|
package com.sukisu.ultra.ui.util.module
|
||||||
|
|
||||||
data class LatestVersionInfo(
|
data class LatestVersionInfo(
|
||||||
val versionCode : Int = 0,
|
val versionCode : Int = 0,
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
|
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 kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import com.sukisu.ultra.ui.util.*
|
||||||
|
|
||||||
|
class KpmViewModel : ViewModel() {
|
||||||
|
var moduleList by mutableStateOf(emptyList<ModuleInfo>())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var search by mutableStateOf("")
|
||||||
|
internal set
|
||||||
|
|
||||||
|
var isRefreshing by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var currentModuleDetail by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun fetchModuleList() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
isRefreshing = true
|
||||||
|
try {
|
||||||
|
val moduleCount = getKpmModuleCount()
|
||||||
|
Log.d("KsuCli", "Module count: $moduleCount")
|
||||||
|
|
||||||
|
moduleList = getAllKpmModuleInfo()
|
||||||
|
|
||||||
|
// 获取 KPM 版本信息
|
||||||
|
val kpmVersion = getKpmVersion()
|
||||||
|
Log.d("KsuCli", "KPM Version: $kpmVersion")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "获取模块列表失败", e)
|
||||||
|
} finally {
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAllKpmModuleInfo(): List<ModuleInfo> {
|
||||||
|
val result = mutableListOf<ModuleInfo>()
|
||||||
|
try {
|
||||||
|
val str = listKpmModules()
|
||||||
|
val moduleNames = str
|
||||||
|
.split("\n")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
for (name in moduleNames) {
|
||||||
|
try {
|
||||||
|
val moduleInfo = parseModuleInfo(name)
|
||||||
|
moduleInfo?.let { result.add(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Error processing module $name", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to get module list", e)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseModuleInfo(name: String): ModuleInfo? {
|
||||||
|
val info = getKpmModuleInfo(name)
|
||||||
|
if (info.isBlank()) return null
|
||||||
|
|
||||||
|
val properties = info.lineSequence()
|
||||||
|
.filter { line ->
|
||||||
|
val trimmed = line.trim()
|
||||||
|
trimmed.isNotEmpty() && !trimmed.startsWith("#")
|
||||||
|
}
|
||||||
|
.mapNotNull { line ->
|
||||||
|
line.split("=", limit = 2).let { parts ->
|
||||||
|
when (parts.size) {
|
||||||
|
2 -> parts[0].trim() to parts[1].trim()
|
||||||
|
1 -> parts[0].trim() to ""
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
|
||||||
|
return ModuleInfo(
|
||||||
|
id = name,
|
||||||
|
name = properties["name"] ?: name,
|
||||||
|
version = properties["version"] ?: "",
|
||||||
|
author = properties["author"] ?: "",
|
||||||
|
description = properties["description"] ?: "",
|
||||||
|
args = properties["args"] ?: "",
|
||||||
|
enabled = true,
|
||||||
|
hasAction = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadModuleDetail(moduleId: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
currentModuleDetail = withContext(Dispatchers.IO) {
|
||||||
|
getKpmModuleInfo(moduleId)
|
||||||
|
}
|
||||||
|
Log.d("KsuCli", "Module detail loaded: $currentModuleDetail")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e("KsuCli", "Failed to load module detail", e)
|
||||||
|
currentModuleDetail = "Error: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var showInputDialog by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var selectedModuleId by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var inputArgs by mutableStateOf("")
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun showInputDialog(moduleId: String) {
|
||||||
|
selectedModuleId = moduleId
|
||||||
|
showInputDialog = true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideInputDialog() {
|
||||||
|
showInputDialog = false
|
||||||
|
selectedModuleId = null
|
||||||
|
inputArgs = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateInputArgs(args: String) {
|
||||||
|
inputArgs = args
|
||||||
|
}
|
||||||
|
|
||||||
|
fun executeControl(): Int {
|
||||||
|
val moduleId = selectedModuleId ?: return -1
|
||||||
|
val result = controlKpmModule(moduleId, inputArgs)
|
||||||
|
hideInputDialog()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModuleInfo(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
val version: String,
|
||||||
|
val author: String,
|
||||||
|
val description: String,
|
||||||
|
val args: String,
|
||||||
|
val enabled: Boolean,
|
||||||
|
val hasAction: Boolean
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -10,8 +10,8 @@ import androidx.lifecycle.ViewModel
|
|||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import shirkneko.zako.sukisu.ui.util.HanziToPinyin
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
import shirkneko.zako.sukisu.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
@@ -40,13 +40,6 @@ class ModuleViewModel : ViewModel() {
|
|||||||
val dirId: String, // real module id (dir name)
|
val dirId: String, // real module id (dir name)
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ModuleUpdateInfo(
|
|
||||||
val version: String,
|
|
||||||
val versionCode: Int,
|
|
||||||
val zipUrl: String,
|
|
||||||
val changelog: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
var search by mutableStateOf("")
|
var search by mutableStateOf("")
|
||||||
@@ -1,11 +1,7 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -14,22 +10,23 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.IKsuInterface
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.ksuApp
|
||||||
import shirkneko.zako.sukisu.ksuApp
|
import com.sukisu.ultra.ui.util.HanziToPinyin
|
||||||
import shirkneko.zako.sukisu.ui.KsuService
|
|
||||||
import shirkneko.zako.sukisu.ui.util.HanziToPinyin
|
|
||||||
import shirkneko.zako.sukisu.ui.util.KsuCli
|
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.coroutines.resume
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
|
||||||
|
import com.sukisu.ultra.ui.webui.packageManager
|
||||||
|
import com.sukisu.ultra.ui.webui.userManager
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
|
|
||||||
class SuperUserViewModel : ViewModel() {
|
class SuperUserViewModel : ViewModel() {
|
||||||
|
val isPlatformAlive get() = Platform.isAlive
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SuperUserViewModel"
|
private const val TAG = "SuperUserViewModel"
|
||||||
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
private var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||||
@@ -68,9 +65,9 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
|
|
||||||
// 批量操作相关状态
|
// 批量操作相关状态
|
||||||
var showBatchActions by mutableStateOf(false)
|
var showBatchActions by mutableStateOf(false)
|
||||||
private set
|
internal set
|
||||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||||
private set
|
internal set
|
||||||
|
|
||||||
private val sortedList by derivedStateOf {
|
private val sortedList by derivedStateOf {
|
||||||
val comparator = compareBy<AppInfo> {
|
val comparator = compareBy<AppInfo> {
|
||||||
@@ -142,55 +139,65 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
fetchAppList() // 刷新列表以显示最新状态
|
fetchAppList() // 刷新列表以显示最新状态
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun connectKsuService(
|
// 批量更新权限和umount模块设置
|
||||||
onDisconnect: () -> Unit = {}
|
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
|
||||||
): Pair<IBinder, ServiceConnection> = suspendCoroutine { continuation ->
|
selectedApps.forEach { packageName ->
|
||||||
val connection = object : ServiceConnection {
|
val app = apps.find { it.packageName == packageName }
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
app?.let {
|
||||||
onDisconnect()
|
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||||
}
|
val updatedProfile = profile.copy(
|
||||||
|
allowSu = allowSu,
|
||||||
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
|
umountModules = umountModules ?: profile.umountModules,
|
||||||
continuation.resume(binder as IBinder to this)
|
nonRootUseDefault = false
|
||||||
|
)
|
||||||
|
if (Natives.setAppProfile(updatedProfile)) {
|
||||||
|
apps = apps.map { app ->
|
||||||
|
if (app.packageName == packageName) {
|
||||||
|
app.copy(profile = updatedProfile)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearSelection()
|
||||||
val intent = Intent(ksuApp, KsuService::class.java)
|
showBatchActions = false // 批量操作完成后退出批量模式
|
||||||
|
fetchAppList() // 刷新列表以显示最新状态
|
||||||
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)
|
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||||
KsuService.stop(intent)
|
apps = apps.map { app ->
|
||||||
|
if (app.packageName == packageName) {
|
||||||
|
app.copy(profile = updatedProfile)
|
||||||
|
} else {
|
||||||
|
app
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun fetchAppList() {
|
suspend fun fetchAppList() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
|
|
||||||
val result = connectKsuService {
|
|
||||||
Log.w(TAG, "KsuService disconnected")
|
|
||||||
}
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
|
withTimeoutOrNull(TIMEOUT_MILLIS) {
|
||||||
|
while (!isPlatformAlive) {
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
} ?: return@withContext // Exit early if timeout
|
||||||
val pm = ksuApp.packageManager
|
val pm = ksuApp.packageManager
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
val binder = result.first
|
val userInfos = Platform.userManager.getUsers()
|
||||||
val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0)
|
val packages = mutableListOf<PackageInfo>()
|
||||||
|
val packageManager = Platform.packageManager
|
||||||
|
|
||||||
withContext(Dispatchers.Main) {
|
for (userInfo in userInfos) {
|
||||||
stopKsuService()
|
Log.i(TAG, "fetchAppList: ${userInfo.id}")
|
||||||
|
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
val packages = allPackages.list
|
|
||||||
|
|
||||||
apps = packages.map {
|
apps = packages.map {
|
||||||
val appInfo = it.applicationInfo
|
val appInfo = it.applicationInfo
|
||||||
val uid = appInfo!!.uid
|
val uid = appInfo!!.uid
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -10,12 +10,12 @@ import androidx.lifecycle.ViewModel
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import shirkneko.zako.sukisu.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import shirkneko.zako.sukisu.profile.Capabilities
|
import com.sukisu.ultra.profile.Capabilities
|
||||||
import shirkneko.zako.sukisu.profile.Groups
|
import com.sukisu.ultra.profile.Groups
|
||||||
import shirkneko.zako.sukisu.ui.util.getAppProfileTemplate
|
import com.sukisu.ultra.ui.util.getAppProfileTemplate
|
||||||
import shirkneko.zako.sukisu.ui.util.listAppProfileTemplates
|
import com.sukisu.ultra.ui.util.listAppProfileTemplates
|
||||||
import shirkneko.zako.sukisu.ui.util.setAppProfileTemplate
|
import com.sukisu.ultra.ui.util.setAppProfileTemplate
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
|
import android.content.ServiceConnection
|
||||||
|
import android.util.Log
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
|
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
|
||||||
|
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val Platform.Companion.packageManager get(): HiddenPackageManager = HiddenPackageManager(this.mService)
|
||||||
|
val Platform.Companion.userManager get(): HiddenUserManager = HiddenUserManager(this.mService)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package shirkneko.zako.sukisu.ui.webui;
|
package com.sukisu.ultra.ui.webui;
|
||||||
|
|
||||||
import java.net.URLConnection;
|
import java.net.URLConnection;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.webui;
|
package com.sukisu.ultra.ui.webui;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.ui.webui
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.app.ActivityManager
|
import android.app.ActivityManager
|
||||||
@@ -15,9 +15,11 @@ import androidx.core.view.ViewCompat
|
|||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.webkit.WebViewAssetLoader
|
import androidx.webkit.WebViewAssetLoader
|
||||||
|
import com.dergoogler.mmrl.platform.model.ModId
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.topjohnwu.superuser.Shell
|
||||||
import shirkneko.zako.sukisu.ui.util.createRootShell
|
import com.sukisu.ultra.ui.util.createRootShell
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
class WebUIActivity : ComponentActivity() {
|
class WebUIActivity : ComponentActivity() {
|
||||||
@@ -41,7 +43,8 @@ class WebUIActivity : ComponentActivity() {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name"))
|
||||||
} else {
|
} else {
|
||||||
val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
val taskDescription =
|
||||||
|
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build()
|
||||||
setTaskDescription(taskDescription)
|
setTaskDescription(taskDescription)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +85,7 @@ class WebUIActivity : ComponentActivity() {
|
|||||||
settings.javaScriptEnabled = true
|
settings.javaScriptEnabled = true
|
||||||
settings.domStorageEnabled = true
|
settings.domStorageEnabled = true
|
||||||
settings.allowFileAccess = false
|
settings.allowFileAccess = false
|
||||||
webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir)
|
webviewInterface = WebViewInterface(WXOptions(this@WebUIActivity, this, ModId(moduleId)))
|
||||||
addJavascriptInterface(webviewInterface, "ksu")
|
addJavascriptInterface(webviewInterface, "ksu")
|
||||||
setWebViewClient(webViewClient)
|
setWebViewClient(webViewClient)
|
||||||
loadUrl("https://mui.kernelsu.org/index.html")
|
loadUrl("https://mui.kernelsu.org/index.html")
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.SystemBarStyle
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.dynamicLightColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.SideEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.paint
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import coil.compose.AsyncImagePainter
|
||||||
|
import coil.compose.rememberAsyncImagePainter
|
||||||
|
import com.sukisu.ultra.ui.theme.ThemeConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.Typography
|
||||||
|
import com.sukisu.ultra.ui.theme.loadCustomBackground
|
||||||
|
|
||||||
|
// 提供界面类型的本地组合
|
||||||
|
val LocalIsSecondaryScreen = staticCompositionLocalOf { false }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebUI专用主题配置
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun WebUIXTheme(
|
||||||
|
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||||
|
dynamicColor: Boolean = true,
|
||||||
|
isSecondaryScreen: Boolean = false,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
|
||||||
|
context.loadCustomBackground()
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val colorScheme = when {
|
||||||
|
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||||
|
if (darkTheme) {
|
||||||
|
dynamicDarkColorScheme(context).let { scheme ->
|
||||||
|
if (isSecondaryScreen) {
|
||||||
|
scheme.copy(
|
||||||
|
background = scheme.surfaceContainerHighest,
|
||||||
|
surface = scheme.surfaceContainerHighest
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
scheme.copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dynamicLightColorScheme(context).let { scheme ->
|
||||||
|
if (isSecondaryScreen) {
|
||||||
|
scheme.copy(
|
||||||
|
background = scheme.surfaceContainerHighest,
|
||||||
|
surface = scheme.surfaceContainerHighest
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
scheme.copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
darkTheme -> {
|
||||||
|
if (isSecondaryScreen) {
|
||||||
|
darkColorScheme().copy(
|
||||||
|
background = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
|
surface = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
darkColorScheme().copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
if (isSecondaryScreen) {
|
||||||
|
lightColorScheme().copy(
|
||||||
|
background = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||||
|
surface = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
lightColorScheme().copy(
|
||||||
|
background = Color.Transparent,
|
||||||
|
surface = Color.Transparent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigureSystemBars(darkTheme)
|
||||||
|
|
||||||
|
val backgroundUri = remember { mutableStateOf(ThemeConfig.customBackgroundUri) }
|
||||||
|
|
||||||
|
LaunchedEffect(ThemeConfig.customBackgroundUri) {
|
||||||
|
backgroundUri.value = ThemeConfig.customBackgroundUri
|
||||||
|
}
|
||||||
|
val bgImagePainter = backgroundUri.value?.let {
|
||||||
|
rememberAsyncImagePainter(
|
||||||
|
model = it,
|
||||||
|
onError = {
|
||||||
|
ThemeConfig.backgroundImageLoaded = false
|
||||||
|
},
|
||||||
|
onSuccess = {
|
||||||
|
ThemeConfig.backgroundImageLoaded = true
|
||||||
|
ThemeConfig.isThemeChanging = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 背景透明度动画
|
||||||
|
val transition = updateTransition(
|
||||||
|
targetState = ThemeConfig.backgroundImageLoaded,
|
||||||
|
label = "bgTransition"
|
||||||
|
)
|
||||||
|
|
||||||
|
val bgAlpha by transition.animateFloat(
|
||||||
|
label = "bgAlpha",
|
||||||
|
transitionSpec = {
|
||||||
|
spring(
|
||||||
|
dampingRatio = 0.8f,
|
||||||
|
stiffness = 300f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { loaded -> if (loaded) 1f else 0f }
|
||||||
|
CompositionLocalProvider(LocalIsSecondaryScreen provides isSecondaryScreen) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = colorScheme,
|
||||||
|
typography = Typography,
|
||||||
|
) {
|
||||||
|
if (isSecondaryScreen) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-2f)
|
||||||
|
.background(if (darkTheme) Color.Black else Color.White)
|
||||||
|
)
|
||||||
|
|
||||||
|
backgroundUri.value?.let { uri ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(-1f)
|
||||||
|
.alpha(bgAlpha)
|
||||||
|
) {
|
||||||
|
bgImagePainter?.let { painter ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.paint(
|
||||||
|
painter = painter,
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
if (darkTheme) Color.Black.copy(alpha = 0.6f)
|
||||||
|
else Color.White.copy(alpha = 0.1f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(
|
||||||
|
Brush.radialGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
if (darkTheme) Color.Black.copy(alpha = 0.5f)
|
||||||
|
else Color.Black.copy(alpha = 0.2f)
|
||||||
|
),
|
||||||
|
radius = 1200f
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.zIndex(1f)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前界面是否为二级界面
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun isSecondaryScreen(): Boolean {
|
||||||
|
return LocalIsSecondaryScreen.current
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置WebUI的系统栏样式
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ConfigureSystemBars(
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
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 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 {
|
||||||
|
WebUIXTheme {
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(Platform.isAlive) {
|
||||||
|
while (!Platform.isAlive) {
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Loading()
|
||||||
|
return@WebUIXTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
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,32 +1,47 @@
|
|||||||
package shirkneko.zako.sukisu.ui.webui
|
package com.sukisu.ultra.ui.webui
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WXOptions
|
||||||
|
import com.dergoogler.mmrl.webui.interfaces.WebUIInterface
|
||||||
|
import com.dergoogler.mmrl.webui.model.JavaScriptInterface
|
||||||
import com.topjohnwu.superuser.CallbackList
|
import com.topjohnwu.superuser.CallbackList
|
||||||
import com.topjohnwu.superuser.ShellUtils
|
import com.topjohnwu.superuser.ShellUtils
|
||||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||||
import shirkneko.zako.sukisu.ui.util.createRootShell
|
import com.sukisu.ultra.ui.util.createRootShell
|
||||||
import shirkneko.zako.sukisu.ui.util.listModules
|
import com.sukisu.ultra.ui.util.listModules
|
||||||
import shirkneko.zako.sukisu.ui.util.withNewRootShell
|
import com.sukisu.ultra.ui.util.withNewRootShell
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import com.sukisu.ultra.ui.util.controlKpmModule
|
||||||
|
import com.sukisu.ultra.ui.util.listKpmModules
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
|
||||||
class WebViewInterface(
|
class WebViewInterface(
|
||||||
val context: Context,
|
wxOptions: WXOptions,
|
||||||
private val webView: WebView,
|
) : WebUIInterface(wxOptions) {
|
||||||
private val modDir: String
|
override var name: String = "ksu"
|
||||||
) {
|
|
||||||
|
companion object {
|
||||||
|
fun factory() = JavaScriptInterface(WebViewInterface::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val modDir get() = "/data/adb/modules/${modId.id}"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@JavascriptInterface
|
||||||
|
fun isSecondaryPage(): Boolean {
|
||||||
|
return isSecondaryScreen()
|
||||||
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun exec(cmd: String): String {
|
fun exec(cmd: String): String {
|
||||||
@@ -168,9 +183,9 @@ class WebViewInterface(
|
|||||||
if (context is Activity) {
|
if (context is Activity) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
hideSystemUI(context.window)
|
hideSystemUI(activity.window)
|
||||||
} else {
|
} else {
|
||||||
showSystemUI(context.window)
|
showSystemUI(activity.window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,6 +212,18 @@ class WebViewInterface(
|
|||||||
}
|
}
|
||||||
return currentModuleInfo.toString()
|
return currentModuleInfo.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================== KPM支持 =============================
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun listAllKpm() : String {
|
||||||
|
return listKpmModules()
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun controlKpm(name: String, args: String) : Int {
|
||||||
|
return controlKpmModule(name, args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideSystemUI(window: Window) =
|
fun hideSystemUI(window: Window) =
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package shirkneko.zako.sukisu.utils
|
package com.sukisu.ultra.utils
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package io.sukisu.ultra;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import com.sukisu.ultra.ui.util.KsuCli;
|
||||||
|
|
||||||
|
public class UltraShellHelper {
|
||||||
|
public static String runCmd(String cmds) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL()
|
||||||
|
.newJob()
|
||||||
|
.add(cmds)
|
||||||
|
.to(new ArrayList<>(), null)
|
||||||
|
.exec()
|
||||||
|
.getOut()) {
|
||||||
|
sb.append(str).append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPathExists(String path) {
|
||||||
|
return runCmd("file " + path).contains("No such file or directory");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CopyFileTo(String path, String target) {
|
||||||
|
runCmd("cp -f " + path + " " + target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package io.sukisu.ultra;
|
||||||
|
|
||||||
|
import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath;
|
||||||
|
import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath;
|
||||||
|
|
||||||
|
public class UltraToolInstall {
|
||||||
|
private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr";
|
||||||
|
private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd";
|
||||||
|
public static void tryToInstall() {
|
||||||
|
String kpmmgrPath = getKpmmgrPath();
|
||||||
|
if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) {
|
||||||
|
UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH);
|
||||||
|
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH);
|
||||||
|
}
|
||||||
|
String SuSFSDaemonPath = getSuSFSDaemonPath();
|
||||||
|
if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) {
|
||||||
|
UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH);
|
||||||
|
UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import coil.Coil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
lateinit var ksuApp: KernelSUApplication
|
|
||||||
|
|
||||||
class KernelSUApplication : Application() {
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
ksuApp = this
|
|
||||||
|
|
||||||
val context = this
|
|
||||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
|
||||||
Coil.setImageLoader(
|
|
||||||
ImageLoader.Builder(context)
|
|
||||||
.components {
|
|
||||||
add(AppIconKeyer())
|
|
||||||
add(AppIconFetcher.Factory(iconSize, false, context))
|
|
||||||
}
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
|
|
||||||
val webroot = File(dataDir, "webroot")
|
|
||||||
if (!webroot.exists()) {
|
|
||||||
webroot.mkdir()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.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 shirkneko.zako.sukisu.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,156 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.activity.ComponentActivity
|
|
||||||
import androidx.activity.compose.setContent
|
|
||||||
import androidx.activity.enableEdgeToEdge
|
|
||||||
import androidx.compose.animation.AnimatedContentTransitionScope
|
|
||||||
import androidx.compose.animation.EnterTransition
|
|
||||||
import androidx.compose.animation.ExitTransition
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.displayCutout
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
|
||||||
import androidx.compose.foundation.layout.union
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.NavigationBar
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHostState
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.luminance
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.navigation.NavBackStackEntry
|
|
||||||
import androidx.navigation.NavHostController
|
|
||||||
import androidx.navigation.compose.rememberNavController
|
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
|
||||||
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
|
|
||||||
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.ksuApp
|
|
||||||
import shirkneko.zako.sukisu.ui.screen.BottomBarDestination
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.KernelSUTheme
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.loadCustomBackground
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.loadThemeMode
|
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
|
||||||
import shirkneko.zako.sukisu.ui.util.rootAvailable
|
|
||||||
import shirkneko.zako.sukisu.ui.util.install
|
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
|
|
||||||
// Enable edge to edge
|
|
||||||
enableEdgeToEdge()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
window.isNavigationBarContrastEnforced = false
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
// 加载保存的背景设置
|
|
||||||
loadCustomBackground()
|
|
||||||
loadThemeMode()
|
|
||||||
CardConfig.load(applicationContext)
|
|
||||||
|
|
||||||
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
if (isManager) install()
|
|
||||||
|
|
||||||
setContent {
|
|
||||||
KernelSUTheme {
|
|
||||||
val navController = rememberNavController()
|
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
|
||||||
Scaffold(
|
|
||||||
bottomBar = { BottomBar(navController) },
|
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
|
||||||
) { innerPadding ->
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalSnackbarHost provides snackBarHostState,
|
|
||||||
) {
|
|
||||||
DestinationsNavHost(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
navGraph = NavGraphs.root,
|
|
||||||
navController = navController,
|
|
||||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
|
||||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
|
||||||
get() = { fadeIn(animationSpec = tween(340)) }
|
|
||||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
|
||||||
get() = { fadeOut(animationSpec = tween(340)) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BottomBar(navController: NavHostController) {
|
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
|
||||||
|
|
||||||
// 获取卡片颜色和透明度
|
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
val cardElevation = CardConfig.cardElevation
|
|
||||||
|
|
||||||
NavigationBar(
|
|
||||||
tonalElevation = cardElevation, // 动态设置阴影
|
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha), // 动态设置颜色和透明度
|
|
||||||
contentColor = if (cardColor.luminance() > 0.5) Color.Black else Color.White, // 根据背景亮度设置文字颜色
|
|
||||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
|
||||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
BottomBarDestination.entries.forEach { destination ->
|
|
||||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
|
||||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
|
||||||
NavigationBarItem(
|
|
||||||
selected = isCurrentDestOnBackStack,
|
|
||||||
onClick = {
|
|
||||||
if (isCurrentDestOnBackStack) {
|
|
||||||
navigator.popBackStack(destination.direction, false)
|
|
||||||
}
|
|
||||||
navigator.navigate(destination.direction) {
|
|
||||||
popUpTo(NavGraphs.root) {
|
|
||||||
saveState = true
|
|
||||||
}
|
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon = {
|
|
||||||
if (isCurrentDestOnBackStack) {
|
|
||||||
Icon(destination.iconSelected, stringResource(destination.label))
|
|
||||||
} else {
|
|
||||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
label = { Text(stringResource(destination.label)) },
|
|
||||||
alwaysShowLabel = false,
|
|
||||||
colors = androidx.compose.material3.NavigationBarItemDefaults.colors(
|
|
||||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.component
|
|
||||||
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SwitchItem(
|
|
||||||
icon: ImageVector,
|
|
||||||
title: String,
|
|
||||||
summary: String,
|
|
||||||
checked: Boolean,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onCheckedChange: (Boolean) -> Unit
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
modifier = modifier,
|
|
||||||
leadingContent = { Icon(icon, contentDescription = null) },
|
|
||||||
headlineContent = { Text(title) },
|
|
||||||
supportingContent = { Text(summary) },
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = checked,
|
|
||||||
onCheckedChange = onCheckedChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.animation.Crossfade
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.AccountCircle
|
|
||||||
import androidx.compose.material.icons.filled.Android
|
|
||||||
import androidx.compose.material.icons.filled.Security
|
|
||||||
import androidx.compose.material3.DropdownMenu
|
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.FilterChip
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.AppProfileConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.RootProfileConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.component.profile.TemplateConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
|
||||||
import shirkneko.zako.sukisu.ui.util.forceStopApp
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getSepolicy
|
|
||||||
import shirkneko.zako.sukisu.ui.util.launchApp
|
|
||||||
import shirkneko.zako.sukisu.ui.util.restartApp
|
|
||||||
import shirkneko.zako.sukisu.ui.util.setSepolicy
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.SuperUserViewModel
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.getTemplateInfoById
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2023/5/16.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun AppProfileScreen(
|
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
appInfo: SuperUserViewModel.AppInfo,
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label)
|
|
||||||
val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label)
|
|
||||||
val suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label)
|
|
||||||
|
|
||||||
val packageName = appInfo.packageName
|
|
||||||
val initialProfile = Natives.getAppProfile(packageName, appInfo.uid)
|
|
||||||
if (initialProfile.allowSu) {
|
|
||||||
initialProfile.rules = getSepolicy(packageName)
|
|
||||||
}
|
|
||||||
var profile by rememberSaveable {
|
|
||||||
mutableStateOf(initialProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
|
||||||
) { paddingValues ->
|
|
||||||
AppProfileInner(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState()),
|
|
||||||
packageName = appInfo.packageName,
|
|
||||||
appLabel = appInfo.label,
|
|
||||||
appIcon = {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(),
|
|
||||||
contentDescription = appInfo.label,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.width(48.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
profile = profile,
|
|
||||||
onViewTemplate = {
|
|
||||||
getTemplateInfoById(it)?.let { info ->
|
|
||||||
navigator.navigate(TemplateEditorScreenDestination(info))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onManageTemplate = {
|
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination())
|
|
||||||
},
|
|
||||||
onProfileChange = {
|
|
||||||
scope.launch {
|
|
||||||
if (it.allowSu) {
|
|
||||||
// sync with allowlist.c - forbid_system_uid
|
|
||||||
if (appInfo.uid < 2000 && appInfo.uid != 1000) {
|
|
||||||
snackBarHost.showSnackbar(suNotAllowed)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) {
|
|
||||||
snackBarHost.showSnackbar(failToUpdateSepolicy)
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!Natives.setAppProfile(it)) {
|
|
||||||
snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid))
|
|
||||||
} else {
|
|
||||||
profile = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AppProfileInner(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
packageName: String,
|
|
||||||
appLabel: String,
|
|
||||||
appIcon: @Composable () -> Unit,
|
|
||||||
profile: Natives.Profile,
|
|
||||||
onViewTemplate: (id: String) -> Unit = {},
|
|
||||||
onManageTemplate: () -> Unit = {},
|
|
||||||
onProfileChange: (Natives.Profile) -> Unit,
|
|
||||||
) {
|
|
||||||
val isRootGranted = profile.allowSu
|
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
|
||||||
AppMenuBox(packageName) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(appLabel) },
|
|
||||||
supportingContent = { Text(packageName) },
|
|
||||||
leadingContent = appIcon,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Security,
|
|
||||||
title = stringResource(id = R.string.superuser),
|
|
||||||
checked = isRootGranted,
|
|
||||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
|
||||||
)
|
|
||||||
|
|
||||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
|
||||||
) {
|
|
||||||
if (current) {
|
|
||||||
val initialMode = if (profile.rootUseDefault) {
|
|
||||||
Mode.Default
|
|
||||||
} else if (profile.rootTemplate != null) {
|
|
||||||
Mode.Template
|
|
||||||
} else {
|
|
||||||
Mode.Custom
|
|
||||||
}
|
|
||||||
var mode by rememberSaveable {
|
|
||||||
mutableStateOf(initialMode)
|
|
||||||
}
|
|
||||||
ProfileBox(mode, true) {
|
|
||||||
// template mode shouldn't change profile here!
|
|
||||||
if (it == Mode.Default || it == Mode.Custom) {
|
|
||||||
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
|
|
||||||
}
|
|
||||||
mode = it
|
|
||||||
}
|
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
|
||||||
if (currentMode == Mode.Template) {
|
|
||||||
TemplateConfig(
|
|
||||||
profile = profile,
|
|
||||||
onViewTemplate = onViewTemplate,
|
|
||||||
onManageTemplate = onManageTemplate,
|
|
||||||
onProfileChange = onProfileChange
|
|
||||||
)
|
|
||||||
} else if (mode == Mode.Custom) {
|
|
||||||
RootProfileConfig(
|
|
||||||
fixedName = true,
|
|
||||||
profile = profile,
|
|
||||||
onProfileChange = onProfileChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
|
||||||
ProfileBox(mode, false) {
|
|
||||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
|
||||||
}
|
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
|
||||||
val modifyEnabled = currentMode == Mode.Custom
|
|
||||||
AppProfileConfig(
|
|
||||||
fixedName = true,
|
|
||||||
profile = profile,
|
|
||||||
enabled = modifyEnabled,
|
|
||||||
onProfileChange = onProfileChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class Mode(@StringRes private val res: Int) {
|
|
||||||
Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom);
|
|
||||||
|
|
||||||
val text: String
|
|
||||||
@Composable get() = stringResource(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
onBack: () -> Unit,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(stringResource(R.string.profile))
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ProfileBox(
|
|
||||||
mode: Mode,
|
|
||||||
hasTemplate: Boolean,
|
|
||||||
onModeChange: (Mode) -> Unit,
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
|
||||||
supportingContent = { Text(mode.text) },
|
|
||||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
|
||||||
)
|
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
|
||||||
ListItem(headlineContent = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
FilterChip(
|
|
||||||
selected = mode == Mode.Default,
|
|
||||||
label = { Text(stringResource(R.string.profile_default)) },
|
|
||||||
onClick = { onModeChange(Mode.Default) },
|
|
||||||
)
|
|
||||||
if (hasTemplate) {
|
|
||||||
FilterChip(
|
|
||||||
selected = mode == Mode.Template,
|
|
||||||
label = { Text(stringResource(R.string.profile_template)) },
|
|
||||||
onClick = { onModeChange(Mode.Template) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
FilterChip(
|
|
||||||
selected = mode == Mode.Custom,
|
|
||||||
label = { Text(stringResource(R.string.profile_custom)) },
|
|
||||||
onClick = { onModeChange(Mode.Custom) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
|
||||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
|
||||||
val density = LocalDensity.current
|
|
||||||
|
|
||||||
BoxWithConstraints(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures {
|
|
||||||
touchPoint = it
|
|
||||||
expanded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
content()
|
|
||||||
|
|
||||||
val (offsetX, offsetY) = with(density) {
|
|
||||||
(touchPoint.x.toDp()) to (touchPoint.y.toDp())
|
|
||||||
}
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
expanded = expanded,
|
|
||||||
offset = DpOffset(offsetX, -offsetY),
|
|
||||||
onDismissRequest = {
|
|
||||||
expanded = false
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
launchApp(packageName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
forceStopApp(packageName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
|
||||||
onClick = {
|
|
||||||
expanded = false
|
|
||||||
restartApp(packageName)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun AppProfilePreview() {
|
|
||||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
|
||||||
AppProfileInner(
|
|
||||||
packageName = "icu.nullptr.test",
|
|
||||||
appLabel = "Test",
|
|
||||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
|
||||||
profile = profile,
|
|
||||||
onProfileChange = {
|
|
||||||
profile = it
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,611 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.PowerManager
|
|
||||||
import android.system.Os
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material.icons.outlined.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.*
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import shirkneko.zako.sukisu.*
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
|
||||||
import shirkneko.zako.sukisu.ui.util.module.LatestVersionInfo
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.getCardElevation
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.shrinkVertically
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>(start = true)
|
|
||||||
@Composable
|
|
||||||
fun HomeScreen(navigator: DestinationsNavigator) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var isSimpleMode by rememberSaveable { mutableStateOf(false) }
|
|
||||||
var isHideVersion by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 从 SharedPreferences 加载简洁模式状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_simple_mode", false)
|
|
||||||
}
|
|
||||||
// 从 SharedPreferences 加载隐藏 KernelSU 版本号开关状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_hide_version", false)
|
|
||||||
}
|
|
||||||
val kernelVersion = getKernelVersion()
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
kernelVersion,
|
|
||||||
onInstallClick = { navigator.navigate(InstallScreenDestination) },
|
|
||||||
onSettingsClick = { navigator.navigate(SettingScreenDestination) },
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
|
||||||
)
|
|
||||||
) { innerPadding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(innerPadding)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 12.dp)
|
|
||||||
.padding(horizontal = 16.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
|
||||||
) {
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
val ksuVersion = if (isManager) Natives.version else null
|
|
||||||
val lkmMode = ksuVersion?.let {
|
|
||||||
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
|
|
||||||
}
|
|
||||||
|
|
||||||
StatusCard(kernelVersion, ksuVersion, lkmMode) {
|
|
||||||
navigator.navigate(InstallScreenDestination)
|
|
||||||
}
|
|
||||||
if (isManager && Natives.requireNewKernel()) {
|
|
||||||
WarningCard(
|
|
||||||
stringResource(id = R.string.require_kernel_version).format(
|
|
||||||
ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (ksuVersion != null && !rootAvailable()) {
|
|
||||||
WarningCard(
|
|
||||||
stringResource(id = R.string.grant_root_failed)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val checkUpdate =
|
|
||||||
LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("check_update", true)
|
|
||||||
if (checkUpdate) {
|
|
||||||
UpdateCard()
|
|
||||||
}
|
|
||||||
val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) }
|
|
||||||
var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) }
|
|
||||||
|
|
||||||
if (!isSimpleMode && clickCount < 3) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = clickCount < 3,
|
|
||||||
exit = shrinkVertically() + fadeOut()
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
clickCount++
|
|
||||||
prefs.edit { putInt("click_count", clickCount) }
|
|
||||||
}
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.using_mksu_manager),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InfoCard()
|
|
||||||
if (!isSimpleMode) {
|
|
||||||
DonateCard()
|
|
||||||
LearnMoreCard()
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(Modifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UpdateCard() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val latestVersionInfo = LatestVersionInfo()
|
|
||||||
val newVersion by produceState(initialValue = latestVersionInfo) {
|
|
||||||
value = withContext(Dispatchers.IO) {
|
|
||||||
checkNewVersion()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentVersionCode = getManagerVersion(context).second
|
|
||||||
val newVersionCode = newVersion.versionCode
|
|
||||||
val newVersionUrl = newVersion.downloadUrl
|
|
||||||
val changelog = newVersion.changelog
|
|
||||||
|
|
||||||
Log.d("UpdateCard", "Current version code: $currentVersionCode")
|
|
||||||
Log.d("UpdateCard", "New version code: $newVersionCode")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val title = stringResource(id = R.string.module_changelog)
|
|
||||||
val updateText = stringResource(id = R.string.module_update)
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = newVersionCode > currentVersionCode,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = shrinkVertically() + fadeOut()
|
|
||||||
) {
|
|
||||||
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
|
|
||||||
WarningCard(
|
|
||||||
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
|
|
||||||
MaterialTheme.colorScheme.outlineVariant
|
|
||||||
) {
|
|
||||||
if (changelog.isEmpty()) {
|
|
||||||
uriHandler.openUri(newVersionUrl)
|
|
||||||
} else {
|
|
||||||
updateDialog.showConfirm(
|
|
||||||
title = title,
|
|
||||||
content = changelog,
|
|
||||||
markdown = true,
|
|
||||||
confirm = updateText
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(id))
|
|
||||||
}, onClick = {
|
|
||||||
reboot(reason)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
kernelVersion: KernelVersion,
|
|
||||||
onInstallClick: () -> Unit,
|
|
||||||
onSettingsClick: () -> Unit,
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.app_name)) },
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
|
||||||
),
|
|
||||||
actions = {
|
|
||||||
if (kernelVersion.isGKI()) {
|
|
||||||
IconButton(onClick = onInstallClick) {
|
|
||||||
Icon(Icons.Filled.Archive, stringResource(R.string.install))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var showDropdown by remember { mutableStateOf(false) }
|
|
||||||
IconButton(onClick = { showDropdown = true }) {
|
|
||||||
Icon(Icons.Filled.Refresh, stringResource(R.string.reboot))
|
|
||||||
DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }
|
|
||||||
) {
|
|
||||||
|
|
||||||
RebootDropdownItem(id = R.string.reboot)
|
|
||||||
|
|
||||||
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
|
|
||||||
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
|
|
||||||
}
|
|
||||||
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
|
|
||||||
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
|
|
||||||
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
|
|
||||||
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StatusCard(
|
|
||||||
kernelVersion: KernelVersion,
|
|
||||||
ksuVersion: Int?,
|
|
||||||
lkmMode: Boolean?,
|
|
||||||
onClickInstall: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
if (kernelVersion.isGKI()) {
|
|
||||||
onClickInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
when {
|
|
||||||
ksuVersion != null -> {
|
|
||||||
val safeMode = when {
|
|
||||||
Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]"
|
|
||||||
else -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
val workingMode = when (lkmMode) {
|
|
||||||
null -> " <Non-GKI>"
|
|
||||||
true -> " <LKM>"
|
|
||||||
else -> " <GKI>"
|
|
||||||
}
|
|
||||||
|
|
||||||
val workingText =
|
|
||||||
"${stringResource(id = R.string.home_working)}$workingMode$safeMode"
|
|
||||||
|
|
||||||
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_hide_version", false)
|
|
||||||
|
|
||||||
Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))
|
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = workingText,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
if (!isHideVersion) {
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_working_version, ksuVersion),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(
|
|
||||||
R.string.home_superuser_count, getSuperuserCount()
|
|
||||||
), style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_module_count, getModuleCount()),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
val translatedStatus = when (suSFS) {
|
|
||||||
"Supported" -> stringResource(R.string.status_supported)
|
|
||||||
"Not Supported" -> stringResource(R.string.status_not_supported)
|
|
||||||
else -> stringResource(R.string.status_unknown)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_susfs, translatedStatus),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kernelVersion.isGKI() -> {
|
|
||||||
Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))
|
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_not_installed),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_click_to_install),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))
|
|
||||||
Column(Modifier.padding(start = 20.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_unsupported),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_unsupported_reason),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun WarningCard(
|
|
||||||
message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
|
|
||||||
.padding(24.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = message, style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LearnMoreCard() {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
val url = stringResource(R.string.home_learn_kernelsu_url)
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
uriHandler.openUri(url)
|
|
||||||
}
|
|
||||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_learn_kernelsu),
|
|
||||||
style = MaterialTheme.typography.titleSmall
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_click_to_learn_kernelsu),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DonateCard() {
|
|
||||||
val uriHandler = LocalUriHandler.current
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
uriHandler.openUri("https://patreon.com/weishu")
|
|
||||||
}
|
|
||||||
.padding(24.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_support_title),
|
|
||||||
style = MaterialTheme.typography.titleSmall
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.home_support_content),
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoCard() {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
.getBoolean("is_simple_mode", false)
|
|
||||||
|
|
||||||
ElevatedCard(
|
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)
|
|
||||||
) {
|
|
||||||
val contents = StringBuilder()
|
|
||||||
val uname = Os.uname()
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun InfoCardItem(
|
|
||||||
label: String,
|
|
||||||
content: String,
|
|
||||||
) {
|
|
||||||
contents.appendLine(label).appendLine(content).appendLine()
|
|
||||||
Text(text = label, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(text = content, style = MaterialTheme.typography.bodyMedium)
|
|
||||||
}
|
|
||||||
|
|
||||||
InfoCardItem(stringResource(R.string.home_kernel), uname.release)
|
|
||||||
|
|
||||||
if (!isSimpleMode) {
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
val androidVersion = Build.VERSION.RELEASE
|
|
||||||
InfoCardItem(stringResource(R.string.home_android_version), androidVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
val deviceModel = getDeviceModel(context)
|
|
||||||
InfoCardItem(stringResource(R.string.home_device_model), deviceModel)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
val managerVersion = getManagerVersion(context)
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_manager_version),
|
|
||||||
"${managerVersion.first} (${managerVersion.second})"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
InfoCardItem(stringResource(R.string.home_selinux_status), getSELinuxStatus())
|
|
||||||
|
|
||||||
|
|
||||||
if (!isSimpleMode) {
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
if (suSFS == "Supported") {
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_susfs_version),
|
|
||||||
"${getSuSFSVersion()} (${stringResource(R.string.manual_hook)})"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val susSUMode = try {
|
|
||||||
susfsSUS_SU_Mode()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (susSUMode == 2 || susSUMode == 0) {
|
|
||||||
val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU"
|
|
||||||
val susSUModeLabel = stringResource(R.string.sus_su_mode)
|
|
||||||
val susSUModeValue = susSUMode.toString()
|
|
||||||
val susSUModeText = if (isSUS_SU) " $susSUModeLabel $susSUModeValue" else ""
|
|
||||||
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_susfs_version),
|
|
||||||
"${getSuSFSVersion()} (${getSuSFSVariant()})$susSUModeText"
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
InfoCardItem(
|
|
||||||
stringResource(R.string.home_susfs_version),
|
|
||||||
"${getSuSFSVersion()} (${stringResource(R.string.manual_hook)})"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getManagerVersion(context: Context): Pair<String, Long> {
|
|
||||||
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
|
|
||||||
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
|
|
||||||
return Pair(packageInfo.versionName!!, versionCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun StatusCardPreview() {
|
|
||||||
Column {
|
|
||||||
StatusCard(KernelVersion(5, 10, 101), 1, null)
|
|
||||||
StatusCard(KernelVersion(5, 10, 101), 20000, true)
|
|
||||||
StatusCard(KernelVersion(5, 10, 101), null, true)
|
|
||||||
StatusCard(KernelVersion(4, 10, 101), null, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun WarningCardPreview() {
|
|
||||||
Column {
|
|
||||||
WarningCard(message = "Warning message")
|
|
||||||
WarningCard(
|
|
||||||
message = "Warning message ",
|
|
||||||
MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
onClick = {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
private fun getDeviceModel(context: Context): String {
|
|
||||||
return try {
|
|
||||||
val systemProperties = Class.forName("android.os.SystemProperties")
|
|
||||||
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
|
|
||||||
val marketNameKeys = listOf(
|
|
||||||
"ro.product.marketname", // Xiaomi
|
|
||||||
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
|
|
||||||
"ro.vivo.market.name", // Vivo
|
|
||||||
"ro.config.marketing_name" // Huawei
|
|
||||||
)
|
|
||||||
for (key in marketNameKeys) {
|
|
||||||
val marketName = getMethod.invoke(null, key, "") as String
|
|
||||||
if (marketName.isNotEmpty()) {
|
|
||||||
return marketName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Build.DEVICE
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Build.DEVICE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,575 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.compose.foundation.LocalIndication
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.selection.toggleable
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.FileUpload
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.Header
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
|
||||||
import com.maxkeppeler.sheets.list.ListDialog
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.DialogHandle
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.util.*
|
|
||||||
import shirkneko.zako.sukisu.utils.AssetsUtil
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2024/3/12.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun InstallScreen(navigator: DestinationsNavigator) {
|
|
||||||
var installMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
|
||||||
var lkmSelection by remember { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var showRebootDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val onFlashComplete = {
|
|
||||||
showRebootDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRebootDialog) {
|
|
||||||
RebootDialog(
|
|
||||||
show = true,
|
|
||||||
onDismiss = { showRebootDialog = false },
|
|
||||||
onConfirm = {
|
|
||||||
showRebootDialog = false
|
|
||||||
try {
|
|
||||||
val process = Runtime.getRuntime().exec("su")
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("svc power reboot\n")
|
|
||||||
writer.write("exit\n")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onInstall = {
|
|
||||||
installMethod?.let { method ->
|
|
||||||
when (method) {
|
|
||||||
is InstallMethod.HorizonKernel -> {
|
|
||||||
method.uri?.let { uri ->
|
|
||||||
val worker = HorizonKernelWorker(context)
|
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
|
||||||
worker.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val flashIt = FlashIt.FlashBoot(
|
|
||||||
boot = if (method is InstallMethod.SelectFile) method.uri else null,
|
|
||||||
lkm = lkmSelection,
|
|
||||||
ota = method is InstallMethod.DirectInstallToInactiveSlot
|
|
||||||
)
|
|
||||||
navigator.navigate(FlashScreenDestination(flashIt))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
val currentKmi by produceState(initialValue = "") {
|
|
||||||
value = getCurrentKmi()
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectKmiDialog = rememberSelectKmiDialog { kmi ->
|
|
||||||
kmi?.let {
|
|
||||||
lkmSelection = LkmSelection.KmiString(it)
|
|
||||||
onInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onClickNext = {
|
|
||||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
|
||||||
selectKmiDialog.show()
|
|
||||||
} else {
|
|
||||||
onInstall()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectLkmLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
it.data?.data?.let { uri ->
|
|
||||||
lkmSelection = LkmSelection.LkmUri(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLkmUpload = {
|
|
||||||
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "application/octet-stream"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
onBack = { navigator.popBackStack() },
|
|
||||||
onLkmUpload = onLkmUpload,
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
|
||||||
)
|
|
||||||
) { innerPadding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(innerPadding)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
SelectInstallMethod { method ->
|
|
||||||
installMethod = method
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
id = R.string.selected_lkm,
|
|
||||||
it.uri.lastPathSegment ?: "(file)"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Button(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
enabled = installMethod != null,
|
|
||||||
onClick = onClickNext
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.install_next),
|
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchHorizonKernelFlash(context: Context, uri: Uri) {
|
|
||||||
val worker = HorizonKernelWorker(context)
|
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener {
|
|
||||||
}
|
|
||||||
worker.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RebootDialog(
|
|
||||||
show: Boolean,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: () -> Unit
|
|
||||||
) {
|
|
||||||
if (show) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(stringResource(id = R.string.reboot_complete_title)) },
|
|
||||||
text = { Text(stringResource(id = R.string.reboot_complete_msg)) },
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onConfirm) {
|
|
||||||
Text(stringResource(id = R.string.yes))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) {
|
|
||||||
Text(stringResource(id = R.string.no))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class HorizonKernelWorker(private val context: Context) : Thread() {
|
|
||||||
var uri: Uri? = null
|
|
||||||
private lateinit var filePath: String
|
|
||||||
private lateinit var binaryPath: String
|
|
||||||
|
|
||||||
|
|
||||||
private var onFlashComplete: (() -> Unit)? = null
|
|
||||||
|
|
||||||
fun setOnFlashCompleteListener(listener: () -> Unit) {
|
|
||||||
onFlashComplete = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun run() {
|
|
||||||
filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}"
|
|
||||||
binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary"
|
|
||||||
|
|
||||||
try {
|
|
||||||
cleanup()
|
|
||||||
if (!rootAvailable()) {
|
|
||||||
showError(context.getString(R.string.root_required))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
copy()
|
|
||||||
if (!File(filePath).exists()) {
|
|
||||||
showError(context.getString(R.string.copy_failed))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
getBinary()
|
|
||||||
patch()
|
|
||||||
flash()
|
|
||||||
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
onFlashComplete?.invoke()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
showError(e.message ?: context.getString(R.string.unknown_error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cleanup() {
|
|
||||||
runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copy() {
|
|
||||||
uri?.let { safeUri ->
|
|
||||||
context.contentResolver.openInputStream(safeUri)?.use { input ->
|
|
||||||
FileOutputStream(File(filePath)).use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBinary() {
|
|
||||||
runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}")
|
|
||||||
if (!File(binaryPath).exists()) {
|
|
||||||
throw IOException("Failed to extract update-binary")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun patch() {
|
|
||||||
val mkbootfsPath = "${context.filesDir.absolutePath}/mkbootfs"
|
|
||||||
AssetsUtil.exportFiles(context, "mkbootfs", mkbootfsPath)
|
|
||||||
runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $mkbootfsPath \$AKHOME/tools;' $binaryPath")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun flash() {
|
|
||||||
val process = ProcessBuilder("su")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
try {
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n")
|
|
||||||
writer.write("sh $binaryPath 3 1 \"$filePath\" && touch ${context.filesDir.absolutePath}/done\nexit\n")
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
process.inputStream.bufferedReader().use { reader ->
|
|
||||||
reader.lineSequence().forEach { line ->
|
|
||||||
if (line.startsWith("ui_print")) {
|
|
||||||
showLog(line.removePrefix("ui_print"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!File("${context.filesDir.absolutePath}/done").exists()) {
|
|
||||||
throw IOException("Flash failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun runCommand(su: Boolean, cmd: String): Int {
|
|
||||||
val process = ProcessBuilder(if (su) "su" else "sh")
|
|
||||||
.redirectErrorStream(true)
|
|
||||||
.start()
|
|
||||||
|
|
||||||
return try {
|
|
||||||
process.outputStream.bufferedWriter().use { writer ->
|
|
||||||
writer.write("$cmd\n")
|
|
||||||
writer.write("exit\n")
|
|
||||||
writer.flush()
|
|
||||||
}
|
|
||||||
process.waitFor()
|
|
||||||
} finally {
|
|
||||||
process.destroy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showError(message: String) {
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showLog(message: String) {
|
|
||||||
(context as? Activity)?.runOnUiThread {
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class InstallMethod {
|
|
||||||
data class SelectFile(
|
|
||||||
val uri: Uri? = null,
|
|
||||||
@StringRes override val label: Int = R.string.select_file,
|
|
||||||
override val summary: String?
|
|
||||||
) : InstallMethod()
|
|
||||||
|
|
||||||
data object DirectInstall : InstallMethod() {
|
|
||||||
override val label: Int
|
|
||||||
get() = R.string.direct_install
|
|
||||||
}
|
|
||||||
|
|
||||||
data object DirectInstallToInactiveSlot : InstallMethod() {
|
|
||||||
override val label: Int
|
|
||||||
get() = R.string.install_inactive_slot
|
|
||||||
}
|
|
||||||
|
|
||||||
data class HorizonKernel(
|
|
||||||
val uri: Uri? = null,
|
|
||||||
@StringRes override val label: Int = R.string.horizon_kernel,
|
|
||||||
override val summary: String? = null
|
|
||||||
) : InstallMethod()
|
|
||||||
|
|
||||||
abstract val label: Int
|
|
||||||
open val summary: String? = null
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|
||||||
val rootAvailable = rootAvailable()
|
|
||||||
val isAbDevice = isAbDevice()
|
|
||||||
val selectFileTip = stringResource(
|
|
||||||
id = R.string.select_file_tip,
|
|
||||||
if (isInitBoot()) "init_boot" else "boot"
|
|
||||||
)
|
|
||||||
|
|
||||||
val radioOptions = mutableListOf<InstallMethod>(
|
|
||||||
InstallMethod.SelectFile(summary = selectFileTip)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (rootAvailable) {
|
|
||||||
radioOptions.add(InstallMethod.DirectInstall)
|
|
||||||
if (isAbDevice) {
|
|
||||||
radioOptions.add(InstallMethod.DirectInstallToInactiveSlot)
|
|
||||||
}
|
|
||||||
radioOptions.add(InstallMethod.HorizonKernel(summary = "Flashing the Anykernel3 Kernel"))
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedOption by remember { mutableStateOf<InstallMethod?>(null) }
|
|
||||||
var currentSelectingMethod by remember { mutableStateOf<InstallMethod?>(null) }
|
|
||||||
|
|
||||||
val selectImageLauncher = rememberLauncherForActivityResult(
|
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
it.data?.data?.let { uri ->
|
|
||||||
val option = when (currentSelectingMethod) {
|
|
||||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
|
||||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = " Flashing the Anykernel3 Kernel")
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
option?.let {
|
|
||||||
selectedOption = it
|
|
||||||
onSelected(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val confirmDialog = rememberConfirmDialog(
|
|
||||||
onConfirm = {
|
|
||||||
selectedOption = InstallMethod.DirectInstallToInactiveSlot
|
|
||||||
onSelected(InstallMethod.DirectInstallToInactiveSlot)
|
|
||||||
},
|
|
||||||
onDismiss = null
|
|
||||||
)
|
|
||||||
|
|
||||||
val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)
|
|
||||||
val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)
|
|
||||||
|
|
||||||
val onClick = { option: InstallMethod ->
|
|
||||||
currentSelectingMethod = option
|
|
||||||
when (option) {
|
|
||||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
|
||||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
||||||
type = "application/*"
|
|
||||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
is InstallMethod.DirectInstall -> {
|
|
||||||
selectedOption = option
|
|
||||||
onSelected(option)
|
|
||||||
}
|
|
||||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
|
||||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
radioOptions.forEach { option ->
|
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.toggleable(
|
|
||||||
value = option.javaClass == selectedOption?.javaClass,
|
|
||||||
onValueChange = { onClick(option) },
|
|
||||||
role = Role.RadioButton,
|
|
||||||
indication = LocalIndication.current,
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = option.javaClass == selectedOption?.javaClass,
|
|
||||||
onClick = { onClick(option) },
|
|
||||||
interactionSource = interactionSource
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = option.label),
|
|
||||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
|
||||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
|
||||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
|
||||||
)
|
|
||||||
option.summary?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
|
||||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|
||||||
return rememberCustomDialog { dismiss ->
|
|
||||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
|
||||||
value = getSupportedKmis()
|
|
||||||
}
|
|
||||||
val options = supportedKmi.map { value ->
|
|
||||||
ListOption(
|
|
||||||
titleText = value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var selection by remember { mutableStateOf<String?>(null) }
|
|
||||||
Surface(
|
|
||||||
color = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
|
||||||
shape = MaterialTheme.shapes.medium
|
|
||||||
) {
|
|
||||||
ListDialog(
|
|
||||||
state = rememberUseCaseState(
|
|
||||||
visible = true,
|
|
||||||
onFinishedRequest = {
|
|
||||||
onSelected(selection)
|
|
||||||
},
|
|
||||||
onCloseRequest = {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
header = Header.Default(
|
|
||||||
title = stringResource(R.string.select_kmi),
|
|
||||||
),
|
|
||||||
selection = ListSelection.Single(
|
|
||||||
showRadioButtons = true,
|
|
||||||
options = options,
|
|
||||||
) { _, option ->
|
|
||||||
selection = option.titleText
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
onBack: () -> Unit = {},
|
|
||||||
onLkmUpload: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.install)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = onBack) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = onLkmUpload) {
|
|
||||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
|
||||||
),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
fun SelectInstallPreview() {
|
|
||||||
InstallScreen(EmptyDestinationsNavigator)
|
|
||||||
}
|
|
||||||
@@ -1,537 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.material3.Slider
|
|
||||||
import androidx.compose.material3.SliderDefaults
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.topjohnwu.superuser.Shell
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.ThemeColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.ThemeConfig
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveCustomBackground
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveThemeColors
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveThemeMode
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.saveDynamicColorState
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getSuSFS
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getSuSFSFeatures
|
|
||||||
import shirkneko.zako.sukisu.ui.util.susfsSUS_SU_0
|
|
||||||
import shirkneko.zako.sukisu.ui.util.susfsSUS_SU_2
|
|
||||||
import shirkneko.zako.sukisu.ui.util.susfsSUS_SU_Mode
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
|
|
||||||
fun saveCardConfig(context: Context) {
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
with(prefs.edit()) {
|
|
||||||
putFloat("card_alpha", CardConfig.cardAlpha)
|
|
||||||
putBoolean("custom_background_enabled", CardConfig.cardElevation == 0.dp)
|
|
||||||
putBoolean("is_custom_alpha_set", CardConfig.isCustomAlphaSet)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val context = LocalContext.current
|
|
||||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
|
||||||
// 主题模式选择
|
|
||||||
var themeMode by remember {
|
|
||||||
mutableIntStateOf(
|
|
||||||
when(ThemeConfig.forceDarkMode) {
|
|
||||||
true -> 2 // 深色
|
|
||||||
false -> 1 // 浅色
|
|
||||||
null -> 0 // 跟随系统
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态颜色开关状态
|
|
||||||
var useDynamicColor by remember {
|
|
||||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
|
||||||
// 主题模式选项
|
|
||||||
val themeOptions = listOf(
|
|
||||||
stringResource(R.string.theme_follow_system),
|
|
||||||
stringResource(R.string.theme_light),
|
|
||||||
stringResource(R.string.theme_dark)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 简洁模块开关状态
|
|
||||||
var isSimpleMode by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新简洁模块开关状态
|
|
||||||
val onSimpleModeChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
|
||||||
isSimpleMode = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核 KernelSU 版本号开关状态
|
|
||||||
var isHideVersion by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核 KernelSU 版本号模块开关状态
|
|
||||||
val onHideVersionChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
|
||||||
isHideVersion = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// SELinux 状态
|
|
||||||
var selinuxEnabled by remember {
|
|
||||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 卡片配置状态
|
|
||||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
|
||||||
var showCardSettings by remember { mutableStateOf(false) }
|
|
||||||
var isCustomBackgroundEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化卡片配置
|
|
||||||
val systemIsDark = isSystemInDarkTheme()
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
CardConfig.apply {
|
|
||||||
cardAlpha = prefs.getFloat("card_alpha", 0.65f)
|
|
||||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
|
||||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
|
||||||
|
|
||||||
// 如果没有手动设置透明度,且是深色模式,则使用默认值
|
|
||||||
if (!isCustomAlphaSet) {
|
|
||||||
val isDarkMode = ThemeConfig.forceDarkMode ?: systemIsDark
|
|
||||||
if (isDarkMode) {
|
|
||||||
cardAlpha = 0.5f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
|
||||||
true -> 2
|
|
||||||
false -> 1
|
|
||||||
null -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主题色选项
|
|
||||||
val themeColorOptions = listOf(
|
|
||||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
|
||||||
stringResource(R.string.color_blue) to ThemeColors.Blue,
|
|
||||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
|
||||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
|
||||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
|
||||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
|
||||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
|
||||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
|
||||||
)
|
|
||||||
|
|
||||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// 图片选择器
|
|
||||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.GetContent()
|
|
||||||
) { uri: Uri? ->
|
|
||||||
uri?.let {
|
|
||||||
context.saveCustomBackground(it)
|
|
||||||
isCustomBackgroundEnabled = true
|
|
||||||
CardConfig.cardElevation = 0.dp
|
|
||||||
saveCardConfig(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.more_settings)) },
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(onClick = { navigator.popBackStack() }) {
|
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(top = 12.dp)
|
|
||||||
) {
|
|
||||||
// SELinux 开关
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Security,
|
|
||||||
title = stringResource(R.string.selinux),
|
|
||||||
summary = if (selinuxEnabled)
|
|
||||||
stringResource(R.string.selinux_enabled) else
|
|
||||||
stringResource(R.string.selinux_disabled),
|
|
||||||
checked = selinuxEnabled
|
|
||||||
) { enabled ->
|
|
||||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
|
||||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
|
||||||
if (result.isSuccess) selinuxEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加简洁模块开关
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.FormatPaint,
|
|
||||||
title = stringResource(R.string.simple_mode),
|
|
||||||
summary = stringResource(R.string.simple_mode_summary),
|
|
||||||
checked = isSimpleMode
|
|
||||||
) {
|
|
||||||
onSimpleModeChange(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核部分版本号
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.FormatPaint,
|
|
||||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
|
||||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
|
||||||
checked = isHideVersion
|
|
||||||
) {
|
|
||||||
onHideVersionChange(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
// region SUSFS 配置(仅在支持时显示)
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
val isSUS_SU = getSuSFSFeatures()
|
|
||||||
if (suSFS == "Supported") {
|
|
||||||
if (isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
|
||||||
// 初始化时,默认启用
|
|
||||||
var isEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(true) // 默认启用
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在启动时检查状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// 如果当前模式不是2就强制启用
|
|
||||||
val currentMode = susfsSUS_SU_Mode()
|
|
||||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
|
||||||
if (currentMode != "2" && wasManuallyDisabled) {
|
|
||||||
susfsSUS_SU_2() // 强制切换到模式2
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
|
||||||
}
|
|
||||||
isEnabled = currentMode == "2"
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.VisibilityOff,
|
|
||||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
|
||||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
|
||||||
checked = isEnabled
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
// 手动启用
|
|
||||||
susfsSUS_SU_2()
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
|
||||||
} else {
|
|
||||||
// 手动关闭
|
|
||||||
susfsSUS_SU_0()
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
|
||||||
}
|
|
||||||
isEnabled = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
// 动态颜色开关
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.ColorLens,
|
|
||||||
title = stringResource(R.string.dynamic_color_title),
|
|
||||||
summary = stringResource(R.string.dynamic_color_summary),
|
|
||||||
checked = useDynamicColor
|
|
||||||
) { enabled ->
|
|
||||||
useDynamicColor = enabled
|
|
||||||
context.saveDynamicColorState(enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 只在未启用动态颜色时显示主题色选择
|
|
||||||
if (!useDynamicColor) {
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Default.Palette, null) },
|
|
||||||
headlineContent = { Text("主题颜色") },
|
|
||||||
supportingContent = {
|
|
||||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
|
||||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
|
||||||
is ThemeColors.Blue -> stringResource(R.string.color_blue)
|
|
||||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
|
||||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
|
||||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
|
||||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
|
||||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
|
||||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
|
||||||
else -> stringResource(R.string.color_default)
|
|
||||||
}
|
|
||||||
Text(currentThemeName)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (showThemeColorDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showThemeColorDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
themeColorOptions.forEach { (name, theme) ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
context.saveThemeColors(when (theme) {
|
|
||||||
ThemeColors.Default -> "default"
|
|
||||||
ThemeColors.Blue -> "blue"
|
|
||||||
ThemeColors.Green -> "green"
|
|
||||||
ThemeColors.Purple -> "purple"
|
|
||||||
ThemeColors.Orange -> "orange"
|
|
||||||
ThemeColors.Pink -> "pink"
|
|
||||||
ThemeColors.Gray -> "gray"
|
|
||||||
ThemeColors.Yellow -> "yellow"
|
|
||||||
else -> "default"
|
|
||||||
})
|
|
||||||
showThemeColorDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.background(theme.Primary, shape = CircleShape)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 自定义背景开关
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Wallpaper, null) },
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
if (isCustomBackgroundEnabled) {
|
|
||||||
showCardSettings = !showCardSettings
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = isCustomBackgroundEnabled,
|
|
||||||
onCheckedChange = { isChecked ->
|
|
||||||
if (isChecked) {
|
|
||||||
pickImageLauncher.launch("image/*")
|
|
||||||
} else {
|
|
||||||
context.saveCustomBackground(null)
|
|
||||||
isCustomBackgroundEnabled = false
|
|
||||||
CardConfig.cardElevation = CardConfig.defaultElevation
|
|
||||||
CardConfig.cardAlpha = 1f
|
|
||||||
CardConfig.isCustomAlphaSet = false
|
|
||||||
saveCardConfig(context)
|
|
||||||
cardAlpha = 0.65f
|
|
||||||
themeMode = 0
|
|
||||||
context.saveThemeMode(null)
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (ThemeConfig.customBackgroundUri != null && showCardSettings) {
|
|
||||||
// 透明度 Slider
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Opacity, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.settings_card_alpha)) },
|
|
||||||
supportingContent = {
|
|
||||||
Slider(
|
|
||||||
value = cardAlpha,
|
|
||||||
onValueChange = { newValue ->
|
|
||||||
cardAlpha = newValue
|
|
||||||
CardConfig.cardAlpha = newValue
|
|
||||||
CardConfig.isCustomAlphaSet = true
|
|
||||||
prefs.edit { putBoolean("is_custom_alpha_set", true) }
|
|
||||||
prefs.edit { putFloat("card_alpha", newValue) }
|
|
||||||
},
|
|
||||||
onValueChangeFinished = {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
saveCardConfig(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
valueRange = 0f..1f,
|
|
||||||
colors = getSliderColors(cardAlpha, useCustomColors = true),
|
|
||||||
thumb = {
|
|
||||||
SliderDefaults.Thumb(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
thumbSize = DpSize(0.dp, 0.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.DarkMode, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
|
||||||
supportingContent = { Text(themeOptions[themeMode]) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
showThemeModeDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
// 主题模式选择对话框
|
|
||||||
if (showThemeModeDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showThemeModeDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.theme_mode)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
themeOptions.forEachIndexed { index, option ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
themeMode = index
|
|
||||||
val newThemeMode = when(index) {
|
|
||||||
0 -> null // 跟随系统
|
|
||||||
1 -> false // 浅色
|
|
||||||
2 -> true // 深色
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
context.saveThemeMode(newThemeMode)
|
|
||||||
when (index) {
|
|
||||||
2 -> {
|
|
||||||
ThemeConfig.forceDarkMode = true
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.isUserDarkModeEnabled = true
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
ThemeConfig.forceDarkMode = false
|
|
||||||
CardConfig.isUserLightModeEnabled = true
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
0 -> {
|
|
||||||
ThemeConfig.forceDarkMode = null
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showThemeModeDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = themeMode == index,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getSliderColors(cardAlpha: Float, useCustomColors: Boolean = false): SliderColors {
|
|
||||||
val theme = ThemeConfig.currentTheme
|
|
||||||
val isDarkTheme = ThemeConfig.forceDarkMode ?: isSystemInDarkTheme()
|
|
||||||
val useDynamicColor = ThemeConfig.useDynamicColor
|
|
||||||
|
|
||||||
return when {
|
|
||||||
// 使用动态颜色时
|
|
||||||
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
|
|
||||||
inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
|
||||||
thumbColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 使用自定义主题色时
|
|
||||||
useCustomColors -> {
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = theme.getCustomSliderActiveColor(),
|
|
||||||
inactiveTrackColor = theme.getCustomSliderInactiveColor(),
|
|
||||||
thumbColor = theme.Primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val activeColor = if (isDarkTheme) {
|
|
||||||
theme.Primary.copy(alpha = cardAlpha)
|
|
||||||
} else {
|
|
||||||
theme.Primary.copy(alpha = cardAlpha)
|
|
||||||
}
|
|
||||||
val inactiveColor = if (isDarkTheme) {
|
|
||||||
Color.DarkGray.copy(alpha = 0.3f)
|
|
||||||
} else {
|
|
||||||
Color.LightGray.copy(alpha = 0.3f)
|
|
||||||
}
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = activeColor,
|
|
||||||
inactiveTrackColor = inactiveColor,
|
|
||||||
thumbColor = activeColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.SnackbarHost
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
|
||||||
import androidx.compose.material3.rememberTopAppBarState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.style.LineHeightStyle
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.Header
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.IconSource
|
|
||||||
import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState
|
|
||||||
import com.maxkeppeler.sheets.list.ListDialog
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListOption
|
|
||||||
import com.maxkeppeler.sheets.list.models.ListSelection
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import shirkneko.zako.sukisu.BuildConfig
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.AboutDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.ConfirmResult
|
|
||||||
import shirkneko.zako.sukisu.ui.component.DialogHandle
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SwitchItem
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberConfirmDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.component.rememberLoadingDialog
|
|
||||||
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
|
|
||||||
import shirkneko.zako.sukisu.ui.util.getBugreportFile
|
|
||||||
import java.time.LocalDateTime
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import shirkneko.zako.sukisu.ui.theme.CardConfig
|
|
||||||
import androidx.core.content.edit
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2023/1/1.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
|
||||||
// region 界面基础设置
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
TopBar(
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
|
||||||
) { paddingValues ->
|
|
||||||
val aboutDialog = rememberCustomDialog {
|
|
||||||
AboutDialog(it)
|
|
||||||
}
|
|
||||||
val loadingDialog = rememberLoadingDialog()
|
|
||||||
val shrinkDialog = rememberConfirmDialog()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
) {
|
|
||||||
// region 上下文与协程
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region 日志导出功能
|
|
||||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.CreateDocument("application/gzip")
|
|
||||||
) { uri: Uri? ->
|
|
||||||
if (uri == null) return@rememberLauncherForActivityResult
|
|
||||||
scope.launch(Dispatchers.IO) {
|
|
||||||
loadingDialog.show()
|
|
||||||
context.contentResolver.openOutputStream(uri)?.use { output ->
|
|
||||||
getBugreportFile(context).inputStream().use {
|
|
||||||
it.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
loadingDialog.hide()
|
|
||||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
// region 配置项列表
|
|
||||||
// 配置文件模板入口
|
|
||||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
|
||||||
headlineContent = { Text(profileTemplate) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// 卸载模块开关
|
|
||||||
var umountChecked by rememberSaveable {
|
|
||||||
mutableStateOf(Natives.isDefaultUmountModules())
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.FolderDelete,
|
|
||||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
|
||||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
|
||||||
checked = umountChecked
|
|
||||||
) {
|
|
||||||
if (Natives.setDefaultUmountModules(it)) {
|
|
||||||
umountChecked = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// SU 禁用开关(仅在兼容版本显示)
|
|
||||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
|
||||||
var isSuDisabled by rememberSaveable {
|
|
||||||
mutableStateOf(!Natives.isSuEnabled())
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.RemoveModerator,
|
|
||||||
title = stringResource(id = R.string.settings_disable_su),
|
|
||||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
|
||||||
checked = isSuDisabled,
|
|
||||||
) { checked ->
|
|
||||||
val shouldEnable = !checked
|
|
||||||
if (Natives.setSuEnabled(shouldEnable)) {
|
|
||||||
isSuDisabled = !shouldEnable
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
|
|
||||||
// 更新检查开关
|
|
||||||
var checkUpdate by rememberSaveable {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getBoolean("check_update", true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Update,
|
|
||||||
title = stringResource(id = R.string.settings_check_update),
|
|
||||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
|
||||||
checked = checkUpdate
|
|
||||||
) {
|
|
||||||
prefs.edit {putBoolean("check_update", it) }
|
|
||||||
checkUpdate = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// Web调试开关
|
|
||||||
var enableWebDebugging by rememberSaveable {
|
|
||||||
mutableStateOf(
|
|
||||||
prefs.getBoolean("enable_web_debugging", false)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.DeveloperMode,
|
|
||||||
title = stringResource(id = R.string.enable_web_debugging),
|
|
||||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
|
||||||
checked = enableWebDebugging
|
|
||||||
) {
|
|
||||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
|
||||||
enableWebDebugging = it
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ExpandMore,
|
|
||||||
contentDescription = newButtonTitle
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(newButtonTitle) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
navigator.navigate(MoreSettingsScreenDestination)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var showBottomsheet by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.BugReport,
|
|
||||||
stringResource(id = R.string.send_log)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
showBottomsheet = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (showBottomsheet) {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = { showBottomsheet = false },
|
|
||||||
content = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(10.dp)
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
|
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.clickable {
|
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
|
||||||
val current = LocalDateTime.now().format(formatter)
|
|
||||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
|
||||||
showBottomsheet = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Save,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.save_log),
|
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
|
||||||
textAlign = TextAlign.Center.also {
|
|
||||||
LineHeightStyle(
|
|
||||||
alignment = LineHeightStyle.Alignment.Center,
|
|
||||||
trim = LineHeightStyle.Trim.None
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Box {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.clickable {
|
|
||||||
scope.launch {
|
|
||||||
val bugreport = loadingDialog.withLoading {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
getBugreportFile(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri: Uri =
|
|
||||||
FileProvider.getUriForFile(
|
|
||||||
context,
|
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
|
||||||
bugreport
|
|
||||||
)
|
|
||||||
|
|
||||||
val shareIntent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
setDataAndType(uri, "application/gzip")
|
|
||||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
}
|
|
||||||
|
|
||||||
context.startActivity(
|
|
||||||
Intent.createChooser(
|
|
||||||
shareIntent,
|
|
||||||
context.getString(R.string.send_log)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Share,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.send_log),
|
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
|
||||||
textAlign = TextAlign.Center.also {
|
|
||||||
LineHeightStyle(
|
|
||||||
alignment = LineHeightStyle.Alignment.Center,
|
|
||||||
trim = LineHeightStyle.Trim.None
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
|
|
||||||
if (lkmMode) {
|
|
||||||
UninstallItem(navigator) {
|
|
||||||
loadingDialog.withLoading(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val about = stringResource(id = R.string.about)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ContactPage,
|
|
||||||
about
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(about) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
aboutDialog.show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun UninstallItem(
|
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
withLoading: suspend (suspend () -> Unit) -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val uninstallConfirmDialog = rememberConfirmDialog()
|
|
||||||
val showTodo = {
|
|
||||||
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
val uninstallDialog = rememberUninstallDialog { uninstallType ->
|
|
||||||
scope.launch {
|
|
||||||
val result = uninstallConfirmDialog.awaitConfirm(
|
|
||||||
title = context.getString(uninstallType.title),
|
|
||||||
content = context.getString(uninstallType.message)
|
|
||||||
)
|
|
||||||
if (result == ConfirmResult.Confirmed) {
|
|
||||||
withLoading {
|
|
||||||
when (uninstallType) {
|
|
||||||
UninstallType.TEMPORARY -> showTodo()
|
|
||||||
UninstallType.PERMANENT -> navigator.navigate(
|
|
||||||
FlashScreenDestination(FlashIt.FlashUninstall)
|
|
||||||
)
|
|
||||||
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
|
|
||||||
FlashScreenDestination(FlashIt.FlashRestore)
|
|
||||||
)
|
|
||||||
UninstallType.NONE -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Delete,
|
|
||||||
uninstall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(uninstall) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
uninstallDialog.show()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
|
|
||||||
TEMPORARY(
|
|
||||||
R.string.settings_uninstall_temporary,
|
|
||||||
R.string.settings_uninstall_temporary_message,
|
|
||||||
Icons.Filled.Delete
|
|
||||||
),
|
|
||||||
PERMANENT(
|
|
||||||
R.string.settings_uninstall_permanent,
|
|
||||||
R.string.settings_uninstall_permanent_message,
|
|
||||||
Icons.Filled.DeleteForever
|
|
||||||
),
|
|
||||||
RESTORE_STOCK_IMAGE(
|
|
||||||
R.string.settings_restore_stock_image,
|
|
||||||
R.string.settings_restore_stock_image_message,
|
|
||||||
Icons.AutoMirrored.Filled.Undo
|
|
||||||
),
|
|
||||||
NONE(0, 0, Icons.Filled.Delete)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|
||||||
return rememberCustomDialog { dismiss ->
|
|
||||||
val options = listOf(
|
|
||||||
// UninstallType.TEMPORARY,
|
|
||||||
UninstallType.PERMANENT,
|
|
||||||
UninstallType.RESTORE_STOCK_IMAGE
|
|
||||||
)
|
|
||||||
val listOptions = options.map {
|
|
||||||
ListOption(
|
|
||||||
titleText = stringResource(it.title),
|
|
||||||
subtitleText = if (it.message != 0) stringResource(it.message) else null,
|
|
||||||
icon = IconSource(it.icon)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var selection = UninstallType.NONE
|
|
||||||
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
|
||||||
if (selection != UninstallType.NONE) {
|
|
||||||
onSelected(selection)
|
|
||||||
}
|
|
||||||
}, onCloseRequest = {
|
|
||||||
dismiss()
|
|
||||||
}), header = Header.Default(
|
|
||||||
title = stringResource(R.string.settings_uninstall),
|
|
||||||
), selection = ListSelection.Single(
|
|
||||||
showRadioButtons = false,
|
|
||||||
options = listOptions,
|
|
||||||
) { index, _ ->
|
|
||||||
selection = options[index]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
TopAppBar(
|
|
||||||
title = { Text(stringResource(R.string.settings)) },
|
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
|
||||||
),
|
|
||||||
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun SettingsPreview() {
|
|
||||||
SettingScreen(EmptyDestinationsNavigator)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,393 +0,0 @@
|
|||||||
package shirkneko.zako.sukisu.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.ExperimentalMaterialApi
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import coil.request.ImageRequest
|
|
||||||
import com.ramcosta.composedestinations.annotation.Destination
|
|
||||||
import com.ramcosta.composedestinations.annotation.RootGraph
|
|
||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination
|
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import shirkneko.zako.sukisu.Natives
|
|
||||||
import shirkneko.zako.sukisu.R
|
|
||||||
import shirkneko.zako.sukisu.ui.component.SearchAppBar
|
|
||||||
import shirkneko.zako.sukisu.ui.util.ModuleModify
|
|
||||||
import shirkneko.zako.sukisu.ui.viewmodel.SuperUserViewModel
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun SuperUserScreen(navigator: DestinationsNavigator) {
|
|
||||||
val viewModel = viewModel<SuperUserViewModel>()
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
val context = LocalContext.current
|
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
|
||||||
|
|
||||||
// 添加备份和还原启动器
|
|
||||||
val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState)
|
|
||||||
val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState)
|
|
||||||
|
|
||||||
LaunchedEffect(key1 = navigator) {
|
|
||||||
viewModel.search = ""
|
|
||||||
if (viewModel.appList.isEmpty()) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(viewModel.search) {
|
|
||||||
if (viewModel.search.isEmpty()) {
|
|
||||||
listState.scrollToItem(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
topBar = {
|
|
||||||
SearchAppBar(
|
|
||||||
title = { Text(stringResource(R.string.superuser)) },
|
|
||||||
searchText = viewModel.search,
|
|
||||||
onSearchTextChange = { viewModel.search = it },
|
|
||||||
onClearClick = { viewModel.search = "" },
|
|
||||||
dropdownContent = {
|
|
||||||
var showDropdown by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
IconButton(
|
|
||||||
onClick = { showDropdown = true },
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.MoreVert,
|
|
||||||
contentDescription = stringResource(id = R.string.settings)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(expanded = showDropdown, onDismissRequest = {
|
|
||||||
showDropdown = false
|
|
||||||
}) {
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(R.string.refresh))
|
|
||||||
}, onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(
|
|
||||||
if (viewModel.showSystemApps) {
|
|
||||||
stringResource(R.string.hide_system_apps)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.show_system_apps)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, onClick = {
|
|
||||||
viewModel.showSystemApps = !viewModel.showSystemApps
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(R.string.backup_allowlist))
|
|
||||||
}, onClick = {
|
|
||||||
backupLauncher.launch(ModuleModify.createAllowlistBackupIntent())
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
DropdownMenuItem(text = {
|
|
||||||
Text(stringResource(R.string.restore_allowlist))
|
|
||||||
}, onClick = {
|
|
||||||
restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent())
|
|
||||||
showDropdown = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
|
||||||
snackbarHost = { SnackbarHost(snackBarHostState) },
|
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
bottomBar = {
|
|
||||||
// 批量操作按钮,直接放在底部栏
|
|
||||||
if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.padding(16.dp),
|
|
||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
|
||||||
) {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.updateBatchPermissions(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.batch_authorization))
|
|
||||||
}
|
|
||||||
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
scope.launch {
|
|
||||||
viewModel.updateBatchPermissions(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.batch_cancel_authorization))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { innerPadding ->
|
|
||||||
PullToRefreshBox(
|
|
||||||
modifier = Modifier.padding(innerPadding),
|
|
||||||
onRefresh = {
|
|
||||||
scope.launch { viewModel.fetchAppList() }
|
|
||||||
},
|
|
||||||
isRefreshing = viewModel.isRefreshing
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
state = listState,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
|
||||||
) {
|
|
||||||
// 获取分组后的应用列表 - 修改分组逻辑,避免应用重复出现在多个分组中
|
|
||||||
val rootApps = viewModel.appList.filter { it.allowSu }
|
|
||||||
val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile }
|
|
||||||
val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile }
|
|
||||||
|
|
||||||
// 显示ROOT权限应用组
|
|
||||||
if (rootApps.isNotEmpty()) {
|
|
||||||
items(rootApps, key = { "root_" + it.packageName + it.uid }) { app ->
|
|
||||||
AppItem(
|
|
||||||
app = app,
|
|
||||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
|
||||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
|
||||||
onSwitchChange = { allowSu ->
|
|
||||||
scope.launch {
|
|
||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
} else {
|
|
||||||
navigator.navigate(AppProfileScreenDestination(app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
// 长按进入多选模式
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleBatchMode()
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示自定义配置应用组
|
|
||||||
if (customApps.isNotEmpty()) {
|
|
||||||
items(customApps, key = { "custom_" + it.packageName + it.uid }) { app ->
|
|
||||||
AppItem(
|
|
||||||
app = app,
|
|
||||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
|
||||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
|
||||||
onSwitchChange = { allowSu ->
|
|
||||||
scope.launch {
|
|
||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
} else {
|
|
||||||
navigator.navigate(AppProfileScreenDestination(app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
// 长按进入多选模式
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleBatchMode()
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示其他应用组
|
|
||||||
if (otherApps.isNotEmpty()) {
|
|
||||||
items(otherApps, key = { "other_" + it.packageName + it.uid }) { app ->
|
|
||||||
AppItem(
|
|
||||||
app = app,
|
|
||||||
isSelected = viewModel.selectedApps.contains(app.packageName),
|
|
||||||
onToggleSelection = { viewModel.toggleAppSelection(app.packageName) },
|
|
||||||
onSwitchChange = { allowSu ->
|
|
||||||
scope.launch {
|
|
||||||
val profile = Natives.getAppProfile(app.packageName, app.uid)
|
|
||||||
val updatedProfile = profile.copy(allowSu = allowSu)
|
|
||||||
if (Natives.setAppProfile(updatedProfile)) {
|
|
||||||
viewModel.fetchAppList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
if (viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
} else {
|
|
||||||
navigator.navigate(AppProfileScreenDestination(app))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = {
|
|
||||||
// 长按进入多选模式
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
viewModel.toggleBatchMode()
|
|
||||||
viewModel.toggleAppSelection(app.packageName)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
viewModel = viewModel
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun GroupHeader(title: String) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
|
||||||
private fun AppItem(
|
|
||||||
app: SuperUserViewModel.AppInfo,
|
|
||||||
isSelected: Boolean,
|
|
||||||
onToggleSelection: () -> Unit,
|
|
||||||
onSwitchChange: (Boolean) -> Unit,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
viewModel: SuperUserViewModel
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
modifier = Modifier
|
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures(
|
|
||||||
onLongPress = { onLongClick() },
|
|
||||||
onTap = { onClick() }
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(app.label) },
|
|
||||||
supportingContent = {
|
|
||||||
Column {
|
|
||||||
Text(app.packageName)
|
|
||||||
FlowRow {
|
|
||||||
if (app.allowSu) {
|
|
||||||
LabelText(label = "ROOT")
|
|
||||||
} else {
|
|
||||||
if (Natives.uidShouldUmount(app.uid)) {
|
|
||||||
LabelText(label = "UMOUNT")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (app.hasCustomProfile) {
|
|
||||||
LabelText(label = "CUSTOM")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingContent = {
|
|
||||||
AsyncImage(
|
|
||||||
model = ImageRequest.Builder(LocalContext.current)
|
|
||||||
.data(app.packageInfo)
|
|
||||||
.crossfade(true)
|
|
||||||
.build(),
|
|
||||||
contentDescription = app.label,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(4.dp)
|
|
||||||
.width(48.dp)
|
|
||||||
.height(48.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
if (!viewModel.showBatchActions) {
|
|
||||||
Switch(
|
|
||||||
checked = app.allowSu,
|
|
||||||
onCheckedChange = onSwitchChange
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Checkbox(
|
|
||||||
checked = isSelected,
|
|
||||||
onCheckedChange = { onToggleSelection() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LabelText(label: String) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 4.dp, end = 4.dp)
|
|
||||||
.background(
|
|
||||||
Color.Black,
|
|
||||||
shape = RoundedCornerShape(4.dp)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = label,
|
|
||||||
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 8.sp,
|
|
||||||
color = Color.White,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user