Compare commits
383 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8b75349e0 | ||
|
|
3f3e520641 | ||
|
|
21aa58f1ee | ||
|
|
da164ebeed | ||
|
|
35ae324df5 | ||
|
|
fdda1f4ec0 | ||
|
|
770ed1fdf2 | ||
|
|
106c10d6f8 | ||
|
|
f10de68deb | ||
|
|
44db32e8de | ||
|
|
3ced30b427 | ||
|
|
624a8d9f86 | ||
|
|
16007f5892 | ||
|
|
6bb83fdb07 | ||
|
|
ec6991f98b | ||
|
|
f6337e2d52 | ||
|
|
6b2bf23946 | ||
|
|
dbc662486b | ||
|
|
e96194c7ff | ||
|
|
49b01aad74 | ||
|
|
656a23a250 | ||
|
|
11a628f536 | ||
|
|
57fcf86579 | ||
|
|
ecb2dae743 | ||
|
|
75e0cd05a9 | ||
|
|
1eb0f19ca6 | ||
|
|
a7ee0423a3 | ||
|
|
355e1c648a | ||
|
|
3cde3e1659 | ||
|
|
8dcc3f7c46 | ||
|
|
a041b90891 | ||
|
|
c1c648e34d | ||
|
|
0754fc8920 | ||
|
|
4d3bae1113 | ||
|
|
0fdd7d437f | ||
|
|
6e89c81407 | ||
|
|
f8b8c7f671 | ||
|
|
cd4edf97bd | ||
|
|
e3f1e49fe1 | ||
|
|
abe0dee4da | ||
|
|
bc3fcec514 | ||
|
|
d225f0bae9 | ||
|
|
48d7a13028 | ||
|
|
7e7d2a28af | ||
|
|
acfba3d0f8 | ||
|
|
bf5a8a8909 | ||
|
|
7c4d8da7d1 | ||
|
|
c656d87e42 | ||
|
|
1b7c7fd726 | ||
|
|
ea68183f80 | ||
|
|
b1ee07fee1 | ||
|
|
c42b4ffe4b | ||
|
|
cfd070f33c | ||
|
|
3ae1a3b10a | ||
|
|
d21f92d817 | ||
|
|
6bb66e2819 | ||
|
|
115206bcc6 | ||
|
|
5ba4f73eeb | ||
|
|
139899d05d | ||
|
|
d2ab325e18 | ||
|
|
be393ddb7c | ||
|
|
467d6e0838 | ||
|
|
46fcf97be3 | ||
|
|
72ed1bc4a2 | ||
|
|
b8544b4f53 | ||
|
|
60f0a721ce | ||
|
|
c62676d643 | ||
|
|
dd2b2e995a | ||
|
|
86456a4d95 | ||
|
|
11e9e37f43 | ||
|
|
a8a2aef4b5 | ||
|
|
77ac0d70fd | ||
|
|
2a00ef96c2 | ||
|
|
39ee1cc41d | ||
|
|
51556d1253 | ||
|
|
94df64f234 | ||
|
|
02f545b3fb | ||
|
|
62c7aac75b | ||
|
|
755d454960 | ||
|
|
9717fa0de6 | ||
|
|
bd6eb7fddd | ||
|
|
9f706873f2 | ||
|
|
c424d5bab4 | ||
|
|
e795387c30 | ||
|
|
044dd9471f | ||
|
|
213a15cdb6 | ||
|
|
a255ea9d56 | ||
|
|
6cbe13dafc | ||
|
|
fb5c7c2f9d | ||
|
|
b8f2d15bd1 | ||
|
|
2bd6929d24 | ||
|
|
42b883240e | ||
|
|
4840540038 | ||
|
|
7bfb37a11e | ||
|
|
dd6d695020 | ||
|
|
6a60b72e21 | ||
|
|
ea3a0cf73b | ||
|
|
1816d15ce8 | ||
|
|
79c8f7a709 | ||
|
|
b0cc0e6f6d | ||
|
|
eccc70c0c9 | ||
|
|
fc828ff3aa | ||
|
|
acb7cfff1b | ||
|
|
3729c22dd0 | ||
|
|
a84cf70730 | ||
|
|
2278fe49d2 | ||
|
|
be14da387e | ||
|
|
1fd13d9d8d | ||
|
|
4205db6870 | ||
|
|
70f03081a4 | ||
|
|
5ccb779b6a | ||
|
|
b07bc408ce | ||
|
|
7ee1fd63f1 | ||
|
|
3551441e42 | ||
|
|
4a1ab76322 | ||
|
|
2fedb051b8 | ||
|
|
10c35f4baa | ||
|
|
4f82eda003 | ||
|
|
80f89c0241 | ||
|
|
8399f14fad | ||
|
|
c49a66d1af | ||
|
|
d66b390361 | ||
|
|
9c290a8080 | ||
|
|
48efc28e8f | ||
|
|
634adad15c | ||
|
|
4532bab230 | ||
|
|
d3c9b6e739 | ||
|
|
8e4f980db0 | ||
|
|
cfee357ed1 | ||
|
|
9393459b27 | ||
|
|
60af173a7e | ||
|
|
23e2377f87 | ||
|
|
d45ba31849 | ||
|
|
c5705c2d5d | ||
|
|
dfae83cf58 | ||
|
|
cd5ba3ac3c | ||
|
|
2c2698f6bc | ||
|
|
f57fe79c5d | ||
|
|
91ae4c9650 | ||
|
|
01f44dc1d9 | ||
|
|
6e35b88041 | ||
|
|
c9c122d79b | ||
|
|
4bec5ae7b1 | ||
|
|
f9b3478dbb | ||
|
|
561c82de0a | ||
|
|
e96ceb84c9 | ||
|
|
ddbbeafc64 | ||
|
|
285478a778 | ||
|
|
00ffa86705 | ||
|
|
74ec20745c | ||
|
|
b7b995bf73 | ||
|
|
29b7f9e0ad | ||
|
|
00a4c69227 | ||
|
|
9c204496c3 | ||
|
|
519401cf39 | ||
|
|
f69eb5c115 | ||
|
|
82e96f4394 | ||
|
|
8e3db00b9b | ||
|
|
adf299d9f3 | ||
|
|
483a39c7ac | ||
|
|
c83baad6d5 | ||
|
|
2ff3b5ee06 | ||
|
|
b537b51034 | ||
|
|
bfb6ea3613 | ||
|
|
edf7685e9a | ||
|
|
f65f62360a | ||
|
|
af97488d58 | ||
|
|
6b1f73aa3d | ||
|
|
4eeece9559 | ||
|
|
4d7d5547ac | ||
|
|
7b74e70f97 | ||
|
|
d92f8fc8fd | ||
|
|
55f9de2fa9 | ||
|
|
a12b14ef46 | ||
|
|
4ce6ff6286 | ||
|
|
ce3566640c | ||
|
|
a0a9fb01f4 | ||
|
|
e1bd16d94f | ||
|
|
776ae8744c | ||
|
|
9285945e8b | ||
|
|
75e56038ec | ||
|
|
730d58f18b | ||
|
|
67a05e8813 | ||
|
|
e95a469bdb | ||
|
|
2ff122e235 | ||
|
|
2319452306 | ||
|
|
a0752d10c7 | ||
|
|
9110d89d61 | ||
|
|
39d6962320 | ||
|
|
7b314116e9 | ||
|
|
ef4101cbf9 | ||
|
|
85f5459c1d | ||
|
|
97e367aa92 | ||
|
|
7097986cf5 | ||
|
|
d6c8ef3737 | ||
|
|
d7a5e80d34 | ||
|
|
2d9783e3d4 | ||
|
|
9f407a94e3 | ||
|
|
99726a2c4e | ||
|
|
f3675e7f6e | ||
|
|
b84d528d99 | ||
|
|
0aab0c1d6b | ||
|
|
ab2367f7fa | ||
|
|
1bac30930f | ||
|
|
6a9186300b | ||
|
|
e6dea3c29e | ||
|
|
c873ff74cb | ||
|
|
7b6f451cfb | ||
|
|
73dea0b8e7 | ||
|
|
f71d617cb3 | ||
|
|
f0d8e42026 | ||
|
|
5bbd95e821 | ||
|
|
fa060dca58 | ||
|
|
9c7ba5b998 | ||
|
|
061136900a | ||
|
|
6375bf4b7c | ||
|
|
17288c086a | ||
|
|
15747ceaa5 | ||
|
|
675bb20f52 | ||
|
|
ec0b26a174 | ||
|
|
92f6f2f51e | ||
|
|
587e73b449 | ||
|
|
07c9cce4b9 | ||
|
|
1d34ea4995 | ||
|
|
d58ec6952c | ||
|
|
50631aade6 | ||
|
|
6df8f6f5d4 | ||
|
|
4aee26b48e | ||
|
|
3bbe415c7e | ||
|
|
892fa9040f | ||
|
|
cadc123eab | ||
|
|
3a27537648 | ||
|
|
6fa1a5c8b8 | ||
|
|
b772c8ece1 | ||
|
|
c0e839dd8e | ||
|
|
a6ed7befdc | ||
|
|
c210b00d54 | ||
|
|
13b5290598 | ||
|
|
b99516da69 | ||
|
|
fe8b5f2135 | ||
|
|
04e1b9bf77 | ||
|
|
b8aaf918fe | ||
|
|
54925188e8 | ||
|
|
3443e48ef1 | ||
|
|
53b3e84890 | ||
|
|
a5b85bfdad | ||
|
|
2817583e3c | ||
|
|
8a6116b4ec | ||
|
|
6a4270787a | ||
|
|
5457a4772b | ||
|
|
ee4c3bb03b | ||
|
|
dd1d17d2cf | ||
|
|
3c353e8f88 | ||
|
|
d743073309 | ||
|
|
a636911612 | ||
|
|
7a62f91752 | ||
|
|
b551a54c8f | ||
|
|
26d86aa2fe | ||
|
|
6ee9246650 | ||
|
|
1cd96fbdbf | ||
|
|
a030a026b1 | ||
|
|
8bf9cd0bee | ||
|
|
13b1aad4b8 | ||
|
|
916d956ce2 | ||
|
|
87a7650d26 | ||
|
|
3484e187da | ||
|
|
0835f330e2 | ||
|
|
8064472477 | ||
|
|
2281012e33 | ||
|
|
83eaeab1ba | ||
|
|
6405764df3 | ||
|
|
253276a27b | ||
|
|
855a71ac56 | ||
|
|
96dc53977f | ||
|
|
31111e68eb | ||
|
|
ac0de29872 | ||
|
|
9e2b722491 | ||
|
|
59627e6fe2 | ||
|
|
cd0b5fb378 | ||
|
|
48a3c64c7c | ||
|
|
62da804518 | ||
|
|
439b99cc4a | ||
|
|
64f0efc2c0 | ||
|
|
f196bf5b76 | ||
|
|
790968be6a | ||
|
|
83f0f9537f | ||
|
|
68ebfec918 | ||
|
|
8be4dea081 | ||
|
|
cfdbba45c3 | ||
|
|
d408c9f4bf | ||
|
|
8f4c58c4c3 | ||
|
|
7e88e9648f | ||
|
|
4516d136a4 | ||
|
|
1b85dfbed1 | ||
|
|
807ffb419a | ||
|
|
e826f43aed | ||
|
|
d619f5fafc | ||
|
|
b3e2f9b7ff | ||
|
|
99a39c6f52 | ||
|
|
22991e8740 | ||
|
|
7646ecb6f7 | ||
|
|
204db674bb | ||
|
|
99fe6623de | ||
|
|
f1f78d2485 | ||
|
|
b2ae20b796 | ||
|
|
83bd4e9642 | ||
|
|
767349798a | ||
|
|
ae38f4709b | ||
|
|
fc7001a11a | ||
|
|
9924809bdb | ||
|
|
58a4ff94e4 | ||
|
|
29033e9b80 | ||
|
|
ea24daf37c | ||
|
|
ebc16583fb | ||
|
|
2a10b41781 | ||
|
|
d5946047a1 | ||
|
|
4ff46a4911 | ||
|
|
b587216b5e | ||
|
|
245fce167e | ||
|
|
de9b82ffd5 | ||
|
|
e570f402e4 | ||
|
|
9c761b13fa | ||
|
|
cc4b135d20 | ||
|
|
ec5395c787 | ||
|
|
6d60e54a7d | ||
|
|
28aa34c0b6 | ||
|
|
0701967bab | ||
|
|
a76b1eece4 | ||
|
|
8e791c680e | ||
|
|
fc9f2ccf25 | ||
|
|
d4682fb06e | ||
|
|
377ea183a7 | ||
|
|
72361ab8bf | ||
|
|
f708e583c3 | ||
|
|
d753e1dc48 | ||
|
|
315a8a3805 | ||
|
|
129fed9c9f | ||
|
|
0baccb7621 | ||
|
|
842a8aa45a | ||
|
|
d17843479c | ||
|
|
0d70cc8e58 | ||
|
|
4e6cacb206 | ||
|
|
52514ba35b | ||
|
|
4d59ce435e | ||
|
|
b3b7fa6f4d | ||
|
|
c057c16391 | ||
|
|
dee7cc6f2b | ||
|
|
3d0d87cb0c | ||
|
|
6b66d9b3f8 | ||
|
|
a301d94858 | ||
|
|
01199470f2 | ||
|
|
9e7ea19567 | ||
|
|
cdc6a6cb4a | ||
|
|
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 |
5
.github/FUNDING.yml
vendored
5
.github/FUNDING.yml
vendored
@@ -1,5 +1,4 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: tiann
|
github: tiann
|
||||||
patreon: weishu
|
patreon: weishu
|
||||||
custom: https://vxposed.com/donate.html
|
open_collective: sukisu-ultra
|
||||||
|
|
||||||
|
|||||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Feature Request
|
|
||||||
url: https://github.com/tiann/KernelSU/issues/1705
|
|
||||||
about: "We do not accept external Feature Requests, see this link for more details."
|
|
||||||
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
39
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: "Suggest an idea for this project"
|
||||||
|
title: "[Feature]"
|
||||||
|
labels: "feature"
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
id: feature-info
|
||||||
|
attributes:
|
||||||
|
value: "## Feature Infomation"
|
||||||
|
- type: textarea
|
||||||
|
id: feature-main
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "Is your feature request related to a problem? Please describe."
|
||||||
|
description: "A clear and concise description of what the problem is."
|
||||||
|
placeholder: "I'm always frustrated when [...]"
|
||||||
|
- type: textarea
|
||||||
|
id: feature-solution
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "Describe the solution you'd like."
|
||||||
|
description: "A clear and concise description of what you want to happen."
|
||||||
|
- type: textarea
|
||||||
|
id: feature-describe
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
attributes:
|
||||||
|
label: "Describe alternatives you've considered."
|
||||||
|
description: "A clear and concise description of any alternative solutions or features you've considered."
|
||||||
|
- type: textarea
|
||||||
|
id: feature-extra
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
attributes:
|
||||||
|
label: "Additional context"
|
||||||
|
description: "Add any other context or screenshots about the feature request here."
|
||||||
|
|
||||||
74
.github/workflows/build-lkm-local.yml
vendored
Normal file
74
.github/workflows/build-lkm-local.yml
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
name: Build LKM for KernelSU Local
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload to branch"
|
||||||
|
secrets:
|
||||||
|
# username:github_pat
|
||||||
|
TOKEN:
|
||||||
|
required: true
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
upload:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload to branch"
|
||||||
|
jobs:
|
||||||
|
build-lkm:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- version: "android12-5.10"
|
||||||
|
sub_level: 236
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
- version: "android13-5.10"
|
||||||
|
sub_level: 234
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android13-5.15"
|
||||||
|
sub_level: 178
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android14-5.15"
|
||||||
|
sub_level: 178
|
||||||
|
os_patch_level: 2025-03
|
||||||
|
- version: "android14-6.1"
|
||||||
|
sub_level: 134
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
- version: "android15-6.6"
|
||||||
|
sub_level: 87
|
||||||
|
os_patch_level: 2025-05
|
||||||
|
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
|
||||||
|
uses: ./.github/workflows/gki-kernel-local.yml
|
||||||
|
with:
|
||||||
|
version: ${{ matrix.version }}
|
||||||
|
version_name: ${{ matrix.version }}.${{ matrix.sub_level }}
|
||||||
|
tag: ${{ matrix.version }}-${{ matrix.os_patch_level }}
|
||||||
|
os_patch_level: ${{ matrix.os_patch_level }}
|
||||||
|
build_lkm: true
|
||||||
|
|
||||||
|
push-to-branch:
|
||||||
|
needs: [build-lkm]
|
||||||
|
runs-on: self-hosted
|
||||||
|
if: ${{ inputs.upload }}
|
||||||
|
steps:
|
||||||
|
- name: Download all workflow run artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: bin/
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Push to branch LKM
|
||||||
|
run: |
|
||||||
|
cd bin
|
||||||
|
git config --global init.defaultBranch lkm
|
||||||
|
git init
|
||||||
|
git remote add origin https://${{ secrets.TOKEN }}@github.com/${{ github.repository }}
|
||||||
|
git config --local user.name "github-actions[bot]"
|
||||||
|
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
find . -type f
|
||||||
|
git add .
|
||||||
|
git commit -m "Upload LKM from ${{ github.sha }}" -m "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
git push --force --set-upstream origin lkm
|
||||||
24
.github/workflows/build-lkm.yml
vendored
24
.github/workflows/build-lkm.yml
vendored
@@ -24,23 +24,23 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- version: "android12-5.10"
|
- version: "android12-5.10"
|
||||||
sub_level: 233
|
sub_level: 237
|
||||||
os_patch_level: 2025-02
|
os_patch_level: 2025-06
|
||||||
- version: "android13-5.10"
|
- version: "android13-5.10"
|
||||||
sub_level: 234
|
sub_level: 236
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-05
|
||||||
- version: "android13-5.15"
|
- version: "android13-5.15"
|
||||||
sub_level: 178
|
sub_level: 180
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-05
|
||||||
- version: "android14-5.15"
|
- version: "android14-5.15"
|
||||||
sub_level: 178
|
sub_level: 180
|
||||||
os_patch_level: 2025-03
|
os_patch_level: 2025-05
|
||||||
- version: "android14-6.1"
|
- version: "android14-6.1"
|
||||||
sub_level: 129
|
sub_level: 138
|
||||||
os_patch_level: 2025-04
|
os_patch_level: 2025-06
|
||||||
- version: "android15-6.6"
|
- version: "android15-6.6"
|
||||||
sub_level: 82
|
sub_level: 89
|
||||||
os_patch_level: 2025-04
|
os_patch_level: 2025-06
|
||||||
# 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:
|
||||||
|
|||||||
252
.github/workflows/build-manager-manual.yml
vendored
Normal file
252
.github/workflows/build-manager-manual.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: Build Manager Manual
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
build_lkm:
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
default: "auto"
|
||||||
|
options:
|
||||||
|
- "true"
|
||||||
|
- "false"
|
||||||
|
- "auto"
|
||||||
|
description: "Whether to build lkm"
|
||||||
|
upload_lkm:
|
||||||
|
required: true
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: "Whether to upload lkm"
|
||||||
|
jobs:
|
||||||
|
check-build-lkm:
|
||||||
|
runs-on: self-hosted
|
||||||
|
outputs:
|
||||||
|
build_lkm: ${{ steps.check-build.outputs.build_lkm }}
|
||||||
|
upload_lkm: ${{ steps.check-build.outputs.upload_lkm }}
|
||||||
|
steps:
|
||||||
|
- name: check build
|
||||||
|
id: check-build
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then
|
||||||
|
kernel_changed="${{ inputs.build_lkm }}"
|
||||||
|
else
|
||||||
|
kernel_changed=true
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
git config --global init.defaultBranch bot
|
||||||
|
git config --global user.name 'Bot'
|
||||||
|
git config --global user.email 'bot@github.shirkneko.io'
|
||||||
|
git init .
|
||||||
|
git remote add origin https://github.com/${{ github.repository }}
|
||||||
|
CURRENT_COMMIT="${{ github.event.head_commit.id }}"
|
||||||
|
git fetch origin $CURRENT_COMMIT --depth=1
|
||||||
|
git fetch origin lkm --depth=1
|
||||||
|
LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)"
|
||||||
|
LKM_COMMIT="${LKM_COMMIT#Upload LKM from }"
|
||||||
|
LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]')
|
||||||
|
echo "LKM_COMMIT=$LKM_COMMIT"
|
||||||
|
git fetch origin "$LKM_COMMIT" --depth=1
|
||||||
|
git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm-local.yml .github/workflows/build-kernel-*.yml && kernel_changed=false
|
||||||
|
cd ..
|
||||||
|
rm -rf tmp
|
||||||
|
fi
|
||||||
|
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
|
||||||
|
need_upload=true
|
||||||
|
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
|
||||||
|
need_upload="${{ inputs.upload_lkm }}"
|
||||||
|
else
|
||||||
|
need_upload=false
|
||||||
|
fi
|
||||||
|
echo "kernel changed: $kernel_changed"
|
||||||
|
echo "need upload: $need_upload"
|
||||||
|
echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
build-lkm:
|
||||||
|
needs: check-build-lkm
|
||||||
|
uses: ./.github/workflows/build-lkm-local.yml
|
||||||
|
if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }}
|
||||||
|
with:
|
||||||
|
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
|
||||||
|
secrets: inherit
|
||||||
|
build-susfs:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/susfs.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
|
||||||
|
build-kpmmgr:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/kpmmgr.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
|
||||||
|
build-ksud:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: [ check-build-lkm, build-lkm ]
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- target: aarch64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: x86_64-linux-android
|
||||||
|
os: ubuntu-latest
|
||||||
|
- target: armv7-linux-androideabi
|
||||||
|
os: ubuntu-latest
|
||||||
|
uses: ./.github/workflows/ksud.yml
|
||||||
|
with:
|
||||||
|
target: ${{ matrix.target }}
|
||||||
|
os: ${{ matrix.os }}
|
||||||
|
pack_lkm: true
|
||||||
|
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
|
||||||
|
|
||||||
|
build-manager:
|
||||||
|
if: ${{ always() }}
|
||||||
|
needs: build-ksud
|
||||||
|
runs-on: self-hosted
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./manager
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup need_upload
|
||||||
|
id: need_upload
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Write key
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref == 'refs/heads/susfs' || github.ref_type == 'tag' }}
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.KEYSTORE }}" ]; then
|
||||||
|
{
|
||||||
|
echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'
|
||||||
|
echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
|
||||||
|
echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
|
||||||
|
echo KEYSTORE_FILE='key.jks'
|
||||||
|
} >> gradle.properties
|
||||||
|
echo "${{ secrets.KEYSTORE }}" | base64 -d > key.jks
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Download arm64 susfs
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: susfs-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm64 kpmmgr
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: kpmmgr-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm64 ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-aarch64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download x86_64 ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-x86_64-linux-android
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Download arm ksud
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ksud-armv7-linux-androideabi
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Copy ksud to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
mkdir -p app/src/main/jniLibs/x86_64
|
||||||
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
|
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
||||||
|
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
|
||||||
|
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
|
||||||
|
|
||||||
|
- name: Copy kpmmgr to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
|
||||||
|
|
||||||
|
- name: Copy susfs to app jniLibs
|
||||||
|
run: |
|
||||||
|
mkdir -p app/src/main/jniLibs/arm64-v8a
|
||||||
|
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: |
|
||||||
|
export ANDROID_HOME=/root/.android/sdk
|
||||||
|
export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$PATH
|
||||||
|
{
|
||||||
|
echo 'org.gradle.parallel=true'
|
||||||
|
echo 'org.gradle.vfs.watch=true'
|
||||||
|
echo 'org.gradle.jvmargs=-Xmx2048m'
|
||||||
|
echo 'android.native.buildOutput=verbose'
|
||||||
|
} >> gradle.properties
|
||||||
|
sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties
|
||||||
|
./gradlew clean assembleRelease
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: manager
|
||||||
|
path: manager/app/build/outputs/apk/release/*.apk
|
||||||
|
|
||||||
|
- name: Upload mappings
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
|
||||||
|
with:
|
||||||
|
name: "mappings"
|
||||||
|
path: "manager/app/build/outputs/mapping/release/"
|
||||||
|
|
||||||
|
- name: Bot session cache
|
||||||
|
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||||
|
id: bot_session_cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: scripts/ksubot.session
|
||||||
|
key: ${{ runner.os }}-bot-session
|
||||||
|
|
||||||
|
- name: Upload to telegram
|
||||||
|
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
|
||||||
|
env:
|
||||||
|
CHAT_ID: ${{ vars.CHAT_ID }}
|
||||||
|
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||||
|
MESSAGE_THREAD_ID: ${{ vars.MESSAGE_THREAD_ID }}
|
||||||
|
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
|
||||||
|
COMMIT_URL: ${{ github.event.head_commit.url }}
|
||||||
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
TITLE: Manager
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
export VERSION=$(git rev-list --count HEAD)
|
||||||
|
APK=$(find ./app/build/outputs/apk/release -name "*.apk")
|
||||||
|
python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK
|
||||||
|
fi
|
||||||
10
.github/workflows/build-manager.yml
vendored
10
.github/workflows/build-manager.yml
vendored
@@ -119,6 +119,8 @@ jobs:
|
|||||||
os: ubuntu-latest
|
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 }}
|
||||||
@@ -198,12 +200,20 @@ 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
|
||||||
|
mkdir -p app/src/main/jniLibs/armeabi-v7a
|
||||||
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
|
cp -f ../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 ../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: |
|
||||||
|
|||||||
40
.github/workflows/crowdin.yml
vendored
Normal file
40
.github/workflows/crowdin.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: Crowdin Action
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
paths:
|
||||||
|
- 'manager/app/src/main/res/values/strings.xml'
|
||||||
|
- 'manager/app/src/main/res/values-*/strings.xml'
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
synchronize-with-crowdin:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Crowdin Action
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
upload_sources: true
|
||||||
|
upload_translations: true
|
||||||
|
auto_approve_imported: true
|
||||||
|
download_translations: true
|
||||||
|
skip_untranslated_files: false
|
||||||
|
skip_untranslated_strings: true
|
||||||
|
|
||||||
|
create_pull_request: true
|
||||||
|
localization_branch_name: "Crowdin"
|
||||||
|
pull_request_labels: 'enhancement, translation'
|
||||||
|
pull_request_title: 'opt: sync translation from Crowdin'
|
||||||
|
|
||||||
|
config: 'crowdin.yml'
|
||||||
|
crowdin_branch_name: "main"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
|
||||||
252
.github/workflows/gki-kernel-local.yml
vendored
Normal file
252
.github/workflows/gki-kernel-local.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
name: GKI Kernel Build Local
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Output directory of gki,
|
||||||
|
for example: android12-5.10
|
||||||
|
version_name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
With SUBLEVEL of kernel,
|
||||||
|
for example: android12-5.10.66
|
||||||
|
tag:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Part of branch name of common kernel manifest,
|
||||||
|
for example: android12-5.10-2021-11
|
||||||
|
os_patch_level:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Patch level of common kernel manifest,
|
||||||
|
for example: 2021-11
|
||||||
|
default: 2022-05
|
||||||
|
patch_path:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
description: >
|
||||||
|
Directory name of .github/patches/<patch_path>
|
||||||
|
for example: 5.10
|
||||||
|
use_cache:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
embed_ksud:
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
default: ksud-aarch64-linux-android
|
||||||
|
description: >
|
||||||
|
Artifact name of prebuilt ksud to be embedded
|
||||||
|
for example: ksud-aarch64-linux-android
|
||||||
|
debug:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
build_lkm:
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
secrets:
|
||||||
|
BOOT_SIGN_KEY:
|
||||||
|
required: false
|
||||||
|
CHAT_ID:
|
||||||
|
required: false
|
||||||
|
BOT_TOKEN:
|
||||||
|
required: false
|
||||||
|
MESSAGE_THREAD_ID:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build ${{ inputs.version_name }}
|
||||||
|
runs-on: self-hosted
|
||||||
|
env:
|
||||||
|
CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion"
|
||||||
|
CCACHE_NOHASHDIR: "true"
|
||||||
|
CCACHE_HARDLINK: "true"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: KernelSU
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup need_upload
|
||||||
|
id: need_upload
|
||||||
|
run: |
|
||||||
|
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
|
||||||
|
echo "UPLOAD=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "UPLOAD=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Setup kernel source
|
||||||
|
run: |
|
||||||
|
echo "Free space:"
|
||||||
|
df -h
|
||||||
|
cd $GITHUB_WORKSPACE
|
||||||
|
sudo apt-get install repo -y
|
||||||
|
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'
|
||||||
|
mkdir android-kernel && cd android-kernel
|
||||||
|
repo init --depth=1 --u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/manifest -b common-${{ inputs.tag }} --repo-rev=v2.35
|
||||||
|
REMOTE_BRANCH=$(git ls-remote https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/common ${{ inputs.tag }})
|
||||||
|
DEFAULT_MANIFEST_PATH=.repo/manifests/default.xml
|
||||||
|
if grep -q deprecated <<< $REMOTE_BRANCH; then
|
||||||
|
echo "Found deprecated branch: ${{ inputs.tag }}"
|
||||||
|
sed -i 's/"${{ inputs.tag }}"/"deprecated\/${{ inputs.tag }}"/g' $DEFAULT_MANIFEST_PATH
|
||||||
|
cat $DEFAULT_MANIFEST_PATH
|
||||||
|
fi
|
||||||
|
repo --version
|
||||||
|
repo --trace sync -c -j$(nproc --all) --no-tags
|
||||||
|
df -h
|
||||||
|
|
||||||
|
- name: Setup KernelSU
|
||||||
|
env:
|
||||||
|
PATCH_PATH: ${{ inputs.patch_path }}
|
||||||
|
IS_DEBUG_KERNEL: ${{ inputs.debug }}
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/android-kernel
|
||||||
|
echo "[+] KernelSU setup"
|
||||||
|
GKI_ROOT=$(pwd)
|
||||||
|
echo "[+] GKI_ROOT: $GKI_ROOT"
|
||||||
|
echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers"
|
||||||
|
ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu
|
||||||
|
echo "[+] Add KernelSU driver to Makefile"
|
||||||
|
DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile
|
||||||
|
DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig
|
||||||
|
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE"
|
||||||
|
grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG"
|
||||||
|
echo "[+] Apply Compilation Patches"
|
||||||
|
if [ ! -e build/build.sh ]; then
|
||||||
|
GLIBC_VERSION=$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}')
|
||||||
|
echo "GLIBC_VERSION: $GLIBC_VERSION"
|
||||||
|
if [ "$(printf '%s\n' "2.38" "$GLIBC_VERSION" | sort -V | head -n1)" = "2.38" ]; then
|
||||||
|
echo "Patching resolve_btfids/Makefile"
|
||||||
|
cd $GKI_ROOT/common/ && sed -i '/\$(Q)\$(MAKE) -C \$(SUBCMD_SRC) OUTPUT=\$(abspath \$(dir \$@))\/ \$(abspath \$@)/s//$(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))\/ $(abspath $@)/' tools/bpf/resolve_btfids/Makefile || echo "No patch needed."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_DEBUG_KERNEL" = "true" ]; then
|
||||||
|
echo "[+] Enable debug features for kernel"
|
||||||
|
printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile
|
||||||
|
fi
|
||||||
|
repo status
|
||||||
|
echo "[+] KernelSU setup done."
|
||||||
|
|
||||||
|
- name: Symbol magic
|
||||||
|
run: |
|
||||||
|
echo "[+] Export all symbol from abi_gki_aarch64.xml"
|
||||||
|
COMMON_ROOT=$GITHUB_WORKSPACE/android-kernel/common
|
||||||
|
KSU_ROOT=$GITHUB_WORKSPACE/KernelSU
|
||||||
|
ABI_XML=$COMMON_ROOT/android/abi_gki_aarch64.xml
|
||||||
|
SYMBOL_LIST=$COMMON_ROOT/android/abi_gki_aarch64
|
||||||
|
# python3 $KSU_ROOT/scripts/abi_gki_all.py $ABI_XML > $SYMBOL_LIST
|
||||||
|
echo "[+] Add KernelSU symbols"
|
||||||
|
cat $KSU_ROOT/kernel/export_symbol.txt | awk '{sub("[ \t]+","");print " "$0}' >> $SYMBOL_LIST
|
||||||
|
|
||||||
|
- name: Setup ccache
|
||||||
|
if: inputs.use_cache == true
|
||||||
|
uses: hendrikmuhs/ccache-action@v1
|
||||||
|
with:
|
||||||
|
key: gki-kernel-aarch64-${{ inputs.version_name }}
|
||||||
|
max-size: 2G
|
||||||
|
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Setup for LKM
|
||||||
|
if: ${{ inputs.build_lkm == true }}
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
pip install ast-grep-cli
|
||||||
|
sudo apt-get install llvm-15 -y
|
||||||
|
ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c
|
||||||
|
ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c
|
||||||
|
sed -i '/config KSU/,/help/{s/default y/default m/}' common/drivers/kernelsu/Kconfig
|
||||||
|
echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules
|
||||||
|
|
||||||
|
# bazel build, android14-5.15, android14-6.1 use bazel
|
||||||
|
if [ ! -e build/build.sh ]; then
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||||
|
if [ -e common/modules.bzl ]; then
|
||||||
|
sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
TARGET_FILE="build/kernel/build.sh"
|
||||||
|
if [ ! -e "$TARGET_FILE" ]; then
|
||||||
|
TARGET_FILE="build/build.sh"
|
||||||
|
fi
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE"
|
||||||
|
sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE
|
||||||
|
sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE
|
||||||
|
sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Make working directory clean to avoid dirty
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
if [ -e common/BUILD.bazel ]; then
|
||||||
|
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
|
||||||
|
fi
|
||||||
|
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
|
||||||
|
git config --global user.email "bot@kernelsu.org"
|
||||||
|
git config --global user.name "KernelSUBot"
|
||||||
|
cd common/ && git add -A && git commit -a -m "Add KernelSU"
|
||||||
|
repo status
|
||||||
|
|
||||||
|
- name: Build Kernel/LKM
|
||||||
|
working-directory: android-kernel
|
||||||
|
run: |
|
||||||
|
if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then
|
||||||
|
export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }}
|
||||||
|
export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }}
|
||||||
|
fi
|
||||||
|
if [ -e build/build.sh ]; then
|
||||||
|
LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh CC="/usr/bin/ccache clang"
|
||||||
|
else
|
||||||
|
tools/bazel run --disk_cache=/home/runner/.cache/bazel --config=fast --config=stamp --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Prepare artifacts
|
||||||
|
id: prepareArtifacts
|
||||||
|
run: |
|
||||||
|
OUTDIR=android-kernel/out/${{ inputs.version }}/dist
|
||||||
|
if [ ! -e $OUTDIR ]; then
|
||||||
|
OUTDIR=android-kernel/dist
|
||||||
|
fi
|
||||||
|
mkdir output
|
||||||
|
if [ "${{ inputs.build_lkm}}" = "true" ]; then
|
||||||
|
llvm-strip-15 -d $OUTDIR/kernelsu.ko
|
||||||
|
mv $OUTDIR/kernelsu.ko ./output/${{ inputs.version }}_kernelsu.ko
|
||||||
|
else
|
||||||
|
cp $OUTDIR/Image ./output/
|
||||||
|
cp $OUTDIR/Image.lz4 ./output/
|
||||||
|
git clone https://github.com/Kernel-SU/AnyKernel3
|
||||||
|
rm -rf ./AnyKernel3/.git
|
||||||
|
cp $OUTDIR/Image ./AnyKernel3/
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Upload Image and Image.gz
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ inputs.build_lkm == false }}
|
||||||
|
with:
|
||||||
|
name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||||
|
path: ./output/*
|
||||||
|
|
||||||
|
- name: Upload AnyKernel3
|
||||||
|
if: ${{ inputs.build_lkm == false }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }}
|
||||||
|
path: ./AnyKernel3/*
|
||||||
|
|
||||||
|
- name: Upload LKM
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ inputs.build_lkm == true }}
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.version }}-lkm
|
||||||
|
path: ./output/*_kernelsu.ko
|
||||||
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"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
.idea
|
.idea
|
||||||
.vscode
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
|||||||
6
crowdin.yml
Normal file
6
crowdin.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
project_id_env: CROWDIN_PROJECT_ID
|
||||||
|
api_token_env: CROWDIN_API_TOKEN
|
||||||
|
preserve_hierarchy: 1
|
||||||
|
files:
|
||||||
|
- source: /manager/app/src/main/res/values/strings.xml
|
||||||
|
translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
# SukiSU Ultra
|
|
||||||
|
|
||||||
**English** | [简体中文](README.md) | [日本語](README-ja.md)
|
|
||||||
|
|
||||||
|
|
||||||
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
|
|
||||||
|
|
||||||
**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental!
|
|
||||||
|
|
||||||
>
|
|
||||||
> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann)
|
|
||||||
> However, we will be a separately maintained branch of KSU in the future
|
|
||||||
>
|
|
||||||
|
|
||||||
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
|
|
||||||
|
|
||||||
## How to add
|
|
||||||
|
|
||||||
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices)
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the main branch
|
|
||||||
```
|
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to use integrated susfs
|
|
||||||
|
|
||||||
1. Use the susfs-dev branch directly without any patching
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## KPM support
|
|
||||||
|
|
||||||
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support.
|
|
||||||
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
|
|
||||||
|
|
||||||
|
|
||||||
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
|
||||||
|
|
||||||
|
|
||||||
KPM template address: https://github.com/udochina/KPM-Build-Anywhere
|
|
||||||
|
|
||||||
## More links
|
|
||||||
Projects compiled based on Sukisu and susfs
|
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
|
||||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
|
||||||
|
|
||||||
## Hook method
|
|
||||||
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
|
|
||||||
|
|
||||||
1. **KPROBES hook:**
|
|
||||||
- This method only supports GKI (5.10 - 6.x) kernels, and all non-GKI kernels must use manual hooks.
|
|
||||||
- For Loadable Kernel Modules (LKM)
|
|
||||||
- Default hooking method for GKI kernels
|
|
||||||
- Requires `CONFIG_KPROBES=y`.
|
|
||||||
2. **Manual hooks:**
|
|
||||||
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
|
|
||||||
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
|
||||||
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
|
|
||||||
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
### GKI
|
|
||||||
1. such as Xiaomi, Redmi, Samsung, and other devices (does not include manufacturers that modified the kernel like Meizu, OnePlus, RealMe, and OPPO)
|
|
||||||
2. Use the prebuilt GKI kernel, the ones with their name ending with AnyKernel3, mentioned in the 'More Links' section, and then flash it with recoveries like TWRP
|
|
||||||
3. Generally, packages with a plain .zip suffix are universal. However, if your device has a MediaTek processor, you should use the ones with .gz suffix, and packages with .lz4 suffix are dedicated to Google devices.
|
|
||||||
|
|
||||||
### OnePlus
|
|
||||||
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
|
|
||||||
> [!Note]
|
|
||||||
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
|
|
||||||
> - Please search for the processor codename by yourself, usually it is all English without numbers.
|
|
||||||
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
1. Kernel-based `su` and root access management.
|
|
||||||
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. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage.
|
|
||||||
4. Bringing back non-GKI/GKI 1.0 support
|
|
||||||
5. More customization
|
|
||||||
6. Support for KPM kernel modules
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
|
|
||||||
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
|
||||||
|
|
||||||
## Sponsorship list
|
|
||||||
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support
|
|
||||||
- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea
|
|
||||||
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
|
|
||||||
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks
|
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support!
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
- [KernelSU](https://github.com/tiann/KernelSU): original project
|
|
||||||
- [MKSU](https://github.com/5ec1cff/KernelSU): Used 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
|
|
||||||
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU conceptualization
|
|
||||||
- [Magisk](https://github.com/topjohnwu/Magisk): Powerful root utility
|
|
||||||
- [genuine](https://github.com/brevent/genuine/): APK v2 Signature Verification
|
|
||||||
- [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
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# 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 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
|
|
||||||
|
|
||||||
## 追加方法
|
|
||||||
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
メインブランチを使用する場合
|
|
||||||
```
|
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main
|
|
||||||
```
|
|
||||||
## 統合された susfs の使い方
|
|
||||||
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
|
|
||||||
|
|
||||||
## KPM に対応
|
|
||||||
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
|
|
||||||
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
|
|
||||||
|
|
||||||
|
|
||||||
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
|
||||||
|
|
||||||
|
|
||||||
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
|
|
||||||
|
|
||||||
## その他のリンク
|
|
||||||
SukiSU と susfs をベースにコンパイルされたプロジェクトです。
|
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
|
||||||
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
|
||||||
|
|
||||||
## フックの方式
|
|
||||||
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
|
||||||
|
|
||||||
1. **KPROBES フック:**
|
|
||||||
- この方式は GKI (5.10 - 6.x) のカーネルのみに対応しています。GKI 以外のカーネルは手動でフックを使用する必要があります。
|
|
||||||
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
|
||||||
- GKI カーネルのデフォルトとなるフック方式
|
|
||||||
- `CONFIG_KPROBES=y` が必要です。
|
|
||||||
2. **手動でフック:**
|
|
||||||
- GKI (5.10 - 6.x) のカーネルの場合、カーネルの defconfig に `CONFIG_KSU_MANUAL_HOOK=y` を追加して `#ifdef CONFIG_KSU` ではなく `#ifdef CONFIG_KSU_MANUAL_HOOK` を使用して KernelSU フックを保護するようにしてください。
|
|
||||||
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
|
||||||
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
|
|
||||||
- KPROBES を手動で統合する一部の非 GKI デバイスでは手動の VFS フック `new_hook.patch` パッチは不要です。
|
|
||||||
|
|
||||||
|
|
||||||
## 使い方
|
|
||||||
### GKI
|
|
||||||
1. Xiaomi、Redmi、Samsung などのデバイス (Meizu、OnePlus、Realme、OPPO などのカーネルを変更したメーカー以外)
|
|
||||||
2. `その他のリンク`の項目で言及されているカーネル名が、AnyKernel3 で終わるビルド済みの GKI カーネルを TWRP などのリカバリーでフラッシュします。
|
|
||||||
3. 一般的な .zip の接頭辞を持つパッケージは汎用的になります。ただし、デバイスに MediaTek 製の SoC が搭載されている場合は、.gz の接頭辞を持つパッケージを使用する必要があります。その他に .lz4 の接頭辞を持つパッケージは Google 製デバイス専用です。
|
|
||||||
|
|
||||||
### 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 実装での重要な部分となります。
|
|
||||||
152
docs/README.md
152
docs/README.md
@@ -1,121 +1,101 @@
|
|||||||
|
<img align='right' src='zakomonochrome-128.svg' width='100px' alt="logo">
|
||||||
|
|
||||||
# SukiSU Ultra
|
# SukiSU Ultra
|
||||||
|
|
||||||
**简体中文** | [English](README-en.md) | [日本語](README-ja.md)
|
**English** | [简体中文](./zh/README.md) | [日本語](./ja/README.md) | [Türkçe](./tr/README.md)
|
||||||
|
|
||||||
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
|
A kernel-based root solution for Android devices, forked from [`tiann/KernelSU`](https://github.com/tiann/KernelSU), and added some interesting changes.
|
||||||
|
|
||||||
**实验性! 使用风险自负!**
|
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||||
|
[](https://t.me/Sukiksu)
|
||||||
|
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||||
|
[](/LICENSE)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
>
|
1. Kernel-based `su` and root access management
|
||||||
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
|
2. Module system based on [Magic Mount](https://github.com/5ec1cff/KernelSU)
|
||||||
> 但是,我们将会在未来成为一个单独维护的KSU分支
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock up the root power in a cage
|
||||||
>
|
4. Support non-GKI and GKI 1.0
|
||||||
|
5. KPM Support
|
||||||
|
6. Tweaks to the manager theme and the built-in susfs management tool.
|
||||||
|
|
||||||
|
## Compatibility Status
|
||||||
|
|
||||||
## 如何添加
|
- KernelSU (before v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+).
|
||||||
在内核源码的根目录下执行以下命令:
|
|
||||||
|
|
||||||
使用 susfs-dev 分支(已集成susfs,带非GKI设备的支持)
|
- Older kernels (4.4+) are also compatible, but the kernel will have to be built manually.
|
||||||
|
|
||||||
```
|
- With more backports, KernelSU can supports 3.x kernel (3.4-3.18).
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
|
- Currently, only `arm64-v8a`, `armeabi-v7a (bare)` and `X86_64`(some) are supported.
|
||||||
|
|
||||||
使用 main 分支
|
## Installation
|
||||||
```
|
|
||||||
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
|
||||||
```
|
|
||||||
|
|
||||||
## 如何集成 susfs
|
See [`guide/installation.md`](guide/installation.md)
|
||||||
|
|
||||||
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs
|
## Integration
|
||||||
|
|
||||||
## 钩子方法
|
See [`guide/how-to-integrate.md`](guide/how-to-integrate.md)
|
||||||
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
|
|
||||||
|
|
||||||
1. **KPROBES 钩子:**
|
## Translation
|
||||||
- 此方法仅支持 GKI 2.0 (5.10 - 6.x) 内核, 所有非 GKI 2.0 内核都必须使用手动钩子
|
|
||||||
- 用于可加载内核模块 (LKM)
|
|
||||||
- GKI 2.0 内核的默认钩子方法
|
|
||||||
- 需要 `CONFIG_KPROBES=y`
|
|
||||||
2. **手动钩子:**
|
|
||||||
- 对于 GKI 2.0 (5.10 - 6.x) 内核,需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
|
|
||||||
- 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
|
||||||
- backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5
|
|
||||||
- 部分手动集成 KPROBES 的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
|
|
||||||
|
|
||||||
|
If you need to submit a translation for the manager, please go to [Crowdin](https://crowdin.com/project/SukiSU-Ultra).
|
||||||
|
|
||||||
## KPM支持
|
## KPM Support
|
||||||
|
|
||||||
- 我们基于KernelPatch去掉了和KSU重复的功能,保留了KPM支持
|
- Based on KernelPatch, we removed features redundant with KSU and retained only KPM support.
|
||||||
- 我们将会引入更多的兼容APatch的函数来确保KPM功能的完整性
|
- Work in Progress: Expanding APatch compatibility by integrating additional functions to ensure compatibility across different implementations.
|
||||||
|
|
||||||
|
**Open-source repository**: [https://github.com/ShirkNeko/SukiSU_KernelPatch_patch](https://github.com/ShirkNeko/SukiSU_KernelPatch_patch)
|
||||||
|
|
||||||
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
**KPM template**: [https://github.com/udochina/KPM-Build-Anywhere](https://github.com/udochina/KPM-Build-Anywhere)
|
||||||
|
|
||||||
|
|
||||||
KPM模板地址: https://github.com/udochina/KPM-Build-Anywhere
|
|
||||||
|
|
||||||
|
|
||||||
## 更多链接
|
|
||||||
基于 SukiSU 和 susfs 编译的项目
|
|
||||||
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
|
||||||
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
|
||||||
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### GKI
|
|
||||||
1. 适用于如小米红米三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
|
|
||||||
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
|
|
||||||
3. 一般不带后缀的 .zip 压缩包是通用,gz 后缀的为天玑机型专用,lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
|
|
||||||
|
|
||||||
### 一加
|
|
||||||
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
|
|
||||||
|
|
||||||
> [!Note]
|
> [!Note]
|
||||||
> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6
|
>
|
||||||
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
|
> 1. Requires `CONFIG_KPM=y`
|
||||||
> - 分支和配置文件请自行到一加内核开源地址进行填写
|
> 2. Non-GKI devices requires `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y`
|
||||||
|
> 3. For kernels below `4.19`, backporting from `set_memory.h` from `4.19` is required.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
## 特点
|
1. Device stuck upon manager app uninstallation?
|
||||||
|
Uninstall _com.sony.playmemories.mobile_
|
||||||
|
|
||||||
1. 基于内核的 `su` 和 root 访问管理
|
## Sponsor
|
||||||
2. 基于 5ec1cff 的 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统
|
|
||||||
3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里
|
|
||||||
4. 恢复对非 GKI 2.0 内核的支持
|
|
||||||
5. 更多自定义功能
|
|
||||||
6. 对KPM内核模块的支持
|
|
||||||
|
|
||||||
|
- [ShirkNeko](https://afdian.com/a/shirkneko) (maintainer of SukiSU)
|
||||||
|
- [weishu](https://github.com/sponsors/tiann) (author of KernelSU)
|
||||||
|
|
||||||
## 许可证
|
## ShirkNeko's sponsorship list
|
||||||
|
|
||||||
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support.
|
||||||
- 除 `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
|
- [zaoqi123](https://github.com/zaoqi123) Thanks for the milk tea.
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project.
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks.
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) 100 USDT
|
||||||
|
- [Saksham Singla](https://github.com/TypeFlu) Provide and maintain the website
|
||||||
|
- [OukaroMF](https://github.com/OukaroMF) Donation of website domain name
|
||||||
|
|
||||||
## 赞助名单
|
## License
|
||||||
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
|
||||||
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
|
||||||
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
|
||||||
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
|
||||||
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
|
||||||
|
|
||||||
|
- 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.
|
||||||
|
- The images of the files `ic_launcher(?!.*alt.*).*` with anime character sticker are copyrighted by [怡子曰曰](https://space.bilibili.com/10545509), the Brand Intellectual Property in the images is owned by [明风 OuO](https://space.bilibili.com/274939213), and the vectorization is done by @MiRinChan. Before using these files, in addition to complying with [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt), you also need to comply with the authorization of the two authors to use these artistic contents.
|
||||||
|
- Except for the files or directories mentioned above, all other parts are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||||
|
|
||||||
|
## Credit
|
||||||
|
|
||||||
|
- [KernelSU](https://github.com/tiann/KernelSU): upstream
|
||||||
|
- [MKSU](https://github.com/5ec1cff/KernelSU): Magic Mount
|
||||||
|
- [RKSU](https://github.com/rsuntk/KernelsU): support non-GKI
|
||||||
|
- [susfs](https://gitlab.com/simonpunk/susfs4ksu): An addon root hiding kernel patches and userspace module for KernelSU.
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch is a key part of the APatch implementation of the kernel module
|
||||||
|
|
||||||
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
|
<details>
|
||||||
|
<summary>KernelSU's credit</summary>
|
||||||
|
|
||||||
## 贡献
|
- [Kernel-Assisted Superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): The KernelSU idea.
|
||||||
|
- [Magisk](https://github.com/topjohnwu/Magisk): The powerful root tool.
|
||||||
- [KernelSU](https://github.com/tiann/KernelSU):原始项目
|
- [genuine](https://github.com/brevent/genuine/): APK v2 signature validation.
|
||||||
- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills.
|
||||||
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非GKI设备重新进行支持
|
</details>
|
||||||
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统
|
|
||||||
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的构想
|
|
||||||
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
|
|
||||||
- [genuine](https://github.com/brevent/genuine/):APK v2 签名验证
|
|
||||||
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
|
|
||||||
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch是APatch实现内核模块的关键部分
|
|
||||||
|
|||||||
97
docs/guide/how-to-integrate.md
Normal file
97
docs/guide/how-to-integrate.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# Integrate
|
||||||
|
|
||||||
|
SukiSU can be integrated into both _GKI_ and _non-GKI_ kernels and has been backported to _4.14_.
|
||||||
|
|
||||||
|
<!-- It should be 3.4, but backslashxx's syscall manual hook cannot use in SukiSU-->
|
||||||
|
|
||||||
|
Some OEMs' customization could result in as much as 50% of kernel code being out-of-tree code and not from upstream Linux kernels or ACKs. Due to this, the custom nature of _non-GKI_ kernels resulted in significant kernel fragmentation, and we lacked a universal method for building them. Therefore, we cannot provide boot images of _non-GKI_ kernels.
|
||||||
|
|
||||||
|
Prerequisites: open source bootable kernel.
|
||||||
|
|
||||||
|
### Hook method
|
||||||
|
|
||||||
|
1. **KPROBES hook:**
|
||||||
|
|
||||||
|
- Default hook method on GKI kernels.
|
||||||
|
- Requires `# CONFIG_KSU_MANUAL_HOOK is not set` & `CONFIG_KPROBES=y`
|
||||||
|
- Used for Loadable Kernel Module (LKM).
|
||||||
|
|
||||||
|
2. **Manual hook:**
|
||||||
|
|
||||||
|
<!-- - backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5 (v1.5 version is not available at the moment, if you want to use it, please use v1.4 version, or standard KernelSU hooks)-->
|
||||||
|
|
||||||
|
- Requires `CONFIG_KSU_MANUAL_HOOK=y`
|
||||||
|
- Requires [`guide/how-to-integrate.md`](guide/how-to-integrate.md)
|
||||||
|
- Requires [https://github.com/~](https://github.com/tiann/KernelSU/blob/main/website/docs/guide/how-to-integrate-for-non-gki.md#manually-modify-the-kernel-source)
|
||||||
|
|
||||||
|
3. **Tracepoint Hook:**
|
||||||
|
|
||||||
|
- Hook method introduced since SukiSU commit [49b01aad](https://github.com/SukiSU-Ultra/SukiSU-Ultra/commit/49b01aad74bcca6dba5a8a2e053bb54b648eb124)
|
||||||
|
- Requires `CONFIG_KSU_TRACEPOINT_HOOK=y`
|
||||||
|
- Requires [`guide/tracepoint-hook.md`](tracepoint-hook.md)
|
||||||
|
|
||||||
|
<!-- This part refer to [rsuntk/KernelSU](https://github.com/rsuntk/KernelSU). -->
|
||||||
|
|
||||||
|
If you're able to build a bootable kernel, there are two ways to integrate KernelSU into the kernel source code:
|
||||||
|
|
||||||
|
1. Automatically with `kprobe`
|
||||||
|
2. Manually
|
||||||
|
|
||||||
|
## Integrate with kprobe
|
||||||
|
|
||||||
|
Applicable:
|
||||||
|
|
||||||
|
- _GKI_ kernel
|
||||||
|
|
||||||
|
Not applicable:
|
||||||
|
|
||||||
|
- _non-GKI_ kernel
|
||||||
|
|
||||||
|
KernelSU uses kprobe to do kernel hooks. If kprobe runs well in your kernel, it's recommended to use it this way.
|
||||||
|
|
||||||
|
Please refer to this document [https://github.com/~](https://github.com/tiann/KernelSU/blob/main/website/docs/guide/how-to-integrate-for-non-gki.md#integrate-with-kprobe). Although it is titled “for _non-GKI_,” it only applies to _GKI_.
|
||||||
|
|
||||||
|
The execution command for the step that adds KernelSU to your kernel source tree is replaced with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manually modify the kernel source
|
||||||
|
|
||||||
|
Applicable:
|
||||||
|
|
||||||
|
- GKI kernel
|
||||||
|
- non-GKI kernel
|
||||||
|
|
||||||
|
Please refer to this document [https://github.com/~ (Integrate for non-GKI)](https://github.com/tiann/KernelSU/blob/main/website/docs/guide/how-to-integrate-for-non-gki.md#manually-modify-the-kernel-source) and [https://github.com/~ (Build for GKI)](https://kernelsu.org/zh_CN/guide/how-to-build.html) to integrate manually, although first link is titled “for non-GKI,” it also applies to GKI. It can work on them both.
|
||||||
|
|
||||||
|
There is another way to integrate but still work in the process.
|
||||||
|
|
||||||
|
<!-- It is backslashxx's syscall manual hook, but it cannot be used now. -->
|
||||||
|
|
||||||
|
Run command for the step that adds KernelSU(SukiSU) to your kernel source tree is replaced with:
|
||||||
|
|
||||||
|
### GKI kernel
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
### non-GKI kernel
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
|
### GKI / non-GKI kernel with susfs (experiment)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-{{branch}}
|
||||||
|
```
|
||||||
|
|
||||||
|
Branch:
|
||||||
|
|
||||||
|
- `main` (susfs-main)
|
||||||
|
- `test` (susfs-test)
|
||||||
|
- version (for example: susfs-1.5.7, you should check the [branches](https://github.com/SukiSU-Ultra/SukiSU-Ultra/branches))
|
||||||
34
docs/guide/installation.md
Normal file
34
docs/guide/installation.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
You can go to [KernelSU Documentation - Installation](https://kernelsu.org/guide/installation.html) for a reference on how to install it, here are just additional instructions.
|
||||||
|
|
||||||
|
## Installation by loading the Loadable Kernel Module(LKM)
|
||||||
|
|
||||||
|
See [KernelSU Documentation - LKM Installation](https://kernelsu.org/guide/installation.html#lkm-installation)
|
||||||
|
|
||||||
|
Beginning with **Android™** (trademark meaning licensed Google Mobile Services) 12, devices shipping with kernel version 5.10 or higher must ship with the GKI kernel. You may be able to use LKM mode.
|
||||||
|
|
||||||
|
## Installation by installing the kernel
|
||||||
|
|
||||||
|
See [KernelSU Documentation - GKI mode Installation](https://kernelsu.org/guide/installation.html#gki-mode-installation)
|
||||||
|
|
||||||
|
We provide pre-built kernels for you to use:
|
||||||
|
|
||||||
|
- [ShirkNeko flavor kernel](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) (add ZRAM compression algorithm patch, susfs, KPM. Works on many devices.)
|
||||||
|
- [MiRinFork flavored kernel](https://github.com/MiRinFork/GKI_SukiSU_SUSFS) (adds susfs, KPM. Closest kernel to GKI, works on most devices.)
|
||||||
|
|
||||||
|
Although some devices can be installed using LKM mode, they cannot be installed on the device by using the GKI kernel; therefore, the kernel needs to be modified manually to compile it. For example:
|
||||||
|
|
||||||
|
- OPPO(OnePlus, REALME)
|
||||||
|
- Meizu
|
||||||
|
|
||||||
|
Also, we provide pre-built kernels for your OnePlus device to use:
|
||||||
|
|
||||||
|
- [ShirkNeko/Action_OnePlus_MKSU_SUSFS](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) (add ZRAM compression algorithm patch, susfs, KPM.)
|
||||||
|
|
||||||
|
Using the link above, Fork into GitHub Action, fill in the build parameters, compile, and finally flush in the zip with the AnyKernel3 suffix.
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> - You only need to fill in the first two parts of the version number, e.g. `5.10`, `6.1`...
|
||||||
|
> - Make sure you know the processor designation, kernel version, etc. before you use it.
|
||||||
270
docs/guide/tracepoint-hook.md
Normal file
270
docs/guide/tracepoint-hook.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Tracepoint Hook Integration
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
Since commit [49b01aad](https://github.com/SukiSU-Ultra/SukiSU-Ultra/commit/49b01aad74bcca6dba5a8a2e053bb54b648eb124), SukiSU has introduced Tracepoint Hook
|
||||||
|
|
||||||
|
This Hook theoretically has lower performance overhead compared to Kprobes Hook, but is inferior to Manual Hook / Syscall Hook
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> This tutorial references the syscall hook v1.4 version from [backslashxx/KernelSU#5](https://github.com/backslashxx/KernelSU/issues/5), as well as the original KernelSU's [Manual Hook](https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source)
|
||||||
|
|
||||||
|
## Guide
|
||||||
|
|
||||||
|
### execve Hook (`exec.c`)
|
||||||
|
|
||||||
|
Generally need to modify the `do_execve` and `compat_do_execve` methods in `fs/exec.c`
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/exec.c
|
||||||
|
+++ b/fs/exec.c
|
||||||
|
@@ -78,6 +78,10 @@
|
||||||
|
#include <trace/hooks/sched.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(task_rename);
|
||||||
|
|
||||||
|
static int bprm_creds_from_file(struct linux_binprm *bprm);
|
||||||
|
@@ -2037,6 +2041,9 @@ static int do_execve(struct filename *filename,
|
||||||
|
{
|
||||||
|
struct user_arg_ptr argv = { .ptr.native = __argv };
|
||||||
|
struct user_arg_ptr envp = { .ptr.native = __envp };
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_execveat_hook((int *)AT_FDCWD, &filename, &argv, &envp, 0);
|
||||||
|
+#endif
|
||||||
|
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -2064,6 +2071,9 @@ static int compat_do_execve(struct filename *filename,
|
||||||
|
.is_compat = true,
|
||||||
|
.ptr.compat = __envp,
|
||||||
|
};
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_execveat_sucompat_hook((int *)AT_FDCWD, &filename, NULL, NULL, NULL); /* 32-bit su */
|
||||||
|
+#endif
|
||||||
|
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### faccessat Hook (`open.c`)
|
||||||
|
|
||||||
|
Generally need to modify the `do_faccessat` method in `/fs/open.c`
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/open.c
|
||||||
|
+++ b/fs/open.c
|
||||||
|
@@ -37,6 +37,10 @@
|
||||||
|
#include "internal.h"
|
||||||
|
#include <trace/hooks/syscall_check.h>
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
int do_truncate(struct user_namespace *mnt_userns, struct dentry *dentry,
|
||||||
|
loff_t length, unsigned int time_attrs, struct file *filp)
|
||||||
|
{
|
||||||
|
@@ -468,6 +472,9 @@ static long do_faccessat(int dfd, const char __user *filename, int mode, int fla
|
||||||
|
|
||||||
|
SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)
|
||||||
|
{
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_faccessat_hook(&dfd, &filename, &mode, NULL);
|
||||||
|
+#endif
|
||||||
|
return do_faccessat(dfd, filename, mode, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If there's no `do_faccessat` method, you can find the `faccessat` SYSCALL definition (for kernels earlier than 4.17)
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/open.c
|
||||||
|
+++ b/fs/open.c
|
||||||
|
@@ -31,6 +31,9 @@
|
||||||
|
#include <linux/ima.h>
|
||||||
|
#include <linux/dnotify.h>
|
||||||
|
#include <linux/compat.h>
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
@@ -369,6 +372,9 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)
|
||||||
|
int res;
|
||||||
|
unsigned int lookup_flags = LOOKUP_FOLLOW;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_faccessat_hook(&dfd, &filename, &mode, NULL);
|
||||||
|
+#endif
|
||||||
|
if (mode & ~S_IRWXO) /* where's F_OK, X_OK, W_OK, R_OK? */
|
||||||
|
return -EINVAL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### sys_read Hook (`read_write.c`)
|
||||||
|
|
||||||
|
Need to modify the `sys_read` method in `fs/read_write.c` (4.19 and above)
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/read_write.c
|
||||||
|
+++ b/fs/read_write.c
|
||||||
|
@@ -25,6 +25,10 @@
|
||||||
|
#include <linux/uaccess.h>
|
||||||
|
#include <asm/unistd.h>
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
const struct file_operations generic_ro_fops = {
|
||||||
|
.llseek = generic_file_llseek,
|
||||||
|
.read_iter = generic_file_read_iter,
|
||||||
|
@@ -630,6 +634,9 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
|
||||||
|
|
||||||
|
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
|
||||||
|
{
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_sys_read_hook(fd, &buf, &count);
|
||||||
|
+#endif
|
||||||
|
return ksys_read(fd, buf, count);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Or the `read` SYSCALL definition (4.14 and below)
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/read_write.c
|
||||||
|
+++ b/fs/read_write.c
|
||||||
|
@@ -25,6 +25,11 @@
|
||||||
|
#include <linux/uaccess.h>
|
||||||
|
#include <asm/unistd.h>
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
+
|
||||||
|
const struct file_operations generic_ro_fops = {
|
||||||
|
.llseek = generic_file_llseek,
|
||||||
|
.read_iter = generic_file_read_iter,
|
||||||
|
@@ -575,6 +580,9 @@ SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
|
||||||
|
|
||||||
|
if (f.file) {
|
||||||
|
loff_t pos = file_pos_read(f.file);
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_sys_read_hook(fd, &buf, &count);
|
||||||
|
+#endif
|
||||||
|
ret = vfs_read(f.file, buf, count, &pos);
|
||||||
|
if (ret >= 0)
|
||||||
|
file_pos_write(f.file, pos);
|
||||||
|
```
|
||||||
|
|
||||||
|
### fstatat Hook (`stat.c`)
|
||||||
|
|
||||||
|
Need to modify the `newfstatat` SYSCALL definition in `stat.c`
|
||||||
|
|
||||||
|
If 32-bit support is needed, also need to modify the `statat64` SYSCALL definition
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/stat.c
|
||||||
|
+++ b/fs/stat.c
|
||||||
|
@@ -24,6 +24,10 @@
|
||||||
|
#include "internal.h"
|
||||||
|
#include "mount.h"
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* generic_fillattr - Fill in the basic attributes from the inode struct
|
||||||
|
* @mnt_userns: user namespace of the mount the inode was found from
|
||||||
|
@@ -408,6 +412,10 @@ SYSCALL_DEFINE4(newfstatat, int, dfd, const char __user *, filename,
|
||||||
|
struct kstat stat;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_stat_hook(&dfd, &filename, &flag);
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
error = vfs_fstatat(dfd, filename, &stat, flag);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
@@ -559,6 +567,10 @@ SYSCALL_DEFINE4(fstatat64, int, dfd, const char __user *, filename,
|
||||||
|
struct kstat stat;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_stat_hook(&dfd, &filename, &flag); /* 32-bit su support */
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
error = vfs_fstatat(dfd, filename, &stat, flag);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
```
|
||||||
|
|
||||||
|
### input Hook (`input.c`, for entering KSU built-in security mode)
|
||||||
|
|
||||||
|
Need to modify the `input_event` method in `drivers/input/input.c`, not `input_handle_event`
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/drivers/input/input.c
|
||||||
|
+++ b/drivers/input/input.c
|
||||||
|
@@ -26,6 +26,10 @@
|
||||||
|
#include "input-compat.h"
|
||||||
|
#include "input-poller.h"
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
MODULE_AUTHOR("Vojtech Pavlik <vojtech@suse.cz>");
|
||||||
|
MODULE_DESCRIPTION("Input core");
|
||||||
|
MODULE_LICENSE("GPL");
|
||||||
|
@@ -451,6 +455,10 @@ void input_event(struct input_dev *dev,
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_input_hook(&type, &code, &value);
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
if (is_event_supported(type, dev->evbit, EV_MAX)) {
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dev->event_lock, flags);
|
||||||
|
```
|
||||||
|
|
||||||
|
### devpts Hook (`pty.c`)
|
||||||
|
|
||||||
|
Need to modify the `pts_unix98_lookup` method in `drivers/tty/pty.c`
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/drivers/tty/pty.c
|
||||||
|
+++ b/drivers/tty/pty.c
|
||||||
|
@@ -31,6 +31,10 @@
|
||||||
|
#include <linux/compat.h>
|
||||||
|
#include "tty.h"
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
#undef TTY_DEBUG_HANGUP
|
||||||
|
#ifdef TTY_DEBUG_HANGUP
|
||||||
|
# define tty_debug_hangup(tty, f, args...) tty_debug(tty, f, ##args)
|
||||||
|
@@ -707,6 +711,10 @@ static struct tty_struct *pts_unix98_lookup(struct tty_driver *driver,
|
||||||
|
{
|
||||||
|
struct tty_struct *tty;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_devpts_hook((struct inode *)file->f_path.dentry->d_inode);
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
mutex_lock(&devpts_mutex);
|
||||||
|
tty = devpts_get_priv(file->f_path.dentry);
|
||||||
|
mutex_unlock(&devpts_mutex);
|
||||||
|
```
|
||||||
151
docs/ja/README.md
Normal file
151
docs/ja/README.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# SukiSU Ultra
|
||||||
|
|
||||||
|
[English](../README.md) | [简体中文](../zh/README.md) | **日本語** | [Türkçe](../tr/README.md)
|
||||||
|
|
||||||
|
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
|
||||||
|
|
||||||
|
**試験中なビルドです!自己責任で使用してください!**<br>
|
||||||
|
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
|
||||||
|
|
||||||
|
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
|
||||||
|
>
|
||||||
|
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
|
||||||
|
|
||||||
|
## 追加する方法
|
||||||
|
|
||||||
|
メインブランチを使用 (非 GKI のデバイスのビルドは非対応) (susfs を手動で統合が必要)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
非 GKI のデバイスに対応するブランチを使用 (susfs を手動で統合が必要)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
|
## 統合された susfs の使い方
|
||||||
|
|
||||||
|
1. susfs-main または他の susfs-\* ブランチを直接で使用、susfs の統合は不要 (非 GKI デバイスのビルドに対応)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
|
||||||
|
```
|
||||||
|
|
||||||
|
## フックの方式
|
||||||
|
|
||||||
|
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
|
||||||
|
|
||||||
|
1. **KPROBES でフック:**
|
||||||
|
|
||||||
|
- 読み込み可能なカーネルモジュールの場合 (LKM)
|
||||||
|
- GKI カーネルのデフォルトとなるフック方式
|
||||||
|
- `CONFIG_KPROBES=y` が必要です
|
||||||
|
|
||||||
|
2. **手動でフック:**
|
||||||
|
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||||
|
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
|
||||||
|
- 非 GKI カーネル用のデフォルトフック方式
|
||||||
|
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
|
||||||
|
|
||||||
|
## KPM に対応
|
||||||
|
|
||||||
|
- 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/非 GKI のインストール`を見つけます。フラッシュする AnyKernel3 の zip ファイルを選択し、フラッシュする実行中のスロットと逆のスロットを選択後に再起動をして GKI モードの更新が保持できます (この方法はすべての非 GKI のデバイスが対応している訳ではないので、自分でお試しください。これは非 GKI のデバイスで TWRP を使用する最も安全な方法です)。
|
||||||
|
- または LKM モードを使用して未使用のスロットにインストールします (OTA 後)。
|
||||||
|
|
||||||
|
## 互換性の状態
|
||||||
|
|
||||||
|
- KernelSU (v1.0.0 より前) は Android GKI 2.0 のデバイス (カーネル 5.10 以降) を公式に対応しています。
|
||||||
|
|
||||||
|
- 古いカーネル (4.4 以降) も互換性がありますが、カーネルを手動で再ビルドする必要があります。
|
||||||
|
|
||||||
|
- KernelSU は追加のリバースポートを通じて 3.x カーネル (3.4-3.18) で対応可能です。
|
||||||
|
|
||||||
|
- 現在 `arm64-v8a`, `armeabi-v7a (bare)` および一部の `X86_64` に対応しています。
|
||||||
|
|
||||||
|
## その他のリンク
|
||||||
|
|
||||||
|
**マネージャーの翻訳を行う場合** https://crowdin.com/project/SukiSU-Ultra
|
||||||
|
|
||||||
|
- [その他パッチ済み GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) ZRAM パッチ、KPM、susfs が含まれています...
|
||||||
|
- [パッチの少ない GKI](https://github.com/MiRinFork/GKI_SukiSU_SUSFS/releases) susfs のみ
|
||||||
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
|
## 使い方
|
||||||
|
|
||||||
|
### Universal GKI
|
||||||
|
|
||||||
|
**すべて**参照してください https://kernelsu.org/ja_JP/guide/installation.html
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス向け (Meizu、OnePlus、Zenith、Oppo などカーネルが変更されているメーカーを除く)
|
||||||
|
> 2. GKI のビルドは[その他のリンク](#その他のリンク)から入手できます。デバイスのカーネルバージョンを確認してください。ダウンロード後に TWRP またはカーネルフラッシュツールを使用して AnyKernel3 の接頭辞を持つ zip ファイルをフラッシュしてください。Pixel のユーザーは、パッチの少ない GKI を使用する必要があります。
|
||||||
|
> 3. 接頭辞のない .zip アーカイブは圧縮されていません。.gz の接頭辞は Tenguet モデルで使用される圧縮になります。
|
||||||
|
|
||||||
|
### OnePlus
|
||||||
|
|
||||||
|
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 カーネルモジュールに対応
|
||||||
|
|
||||||
|
## トラブルシューティング
|
||||||
|
|
||||||
|
1. KernelSU Manager のアンインストールが停止してしまう → com.sony.playmemories.mobile のアプリをアンインストールしてください。
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
- 「kernel」のディレクトリ内のファイルは [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) のライセンスに基づいています。
|
||||||
|
- アニメキャラクター画像とスタンプを含むこれらのファイルの `ic_launcher(?!.*alt.*).*` は[怡子曰曰](https://space.bilibili.com/10545509)によって著作権保護されており、画像の Brand Intellectual Property は[明风 OuO](https://space.bilibili.com/274939213)によって所有され、ベクター化は @MiRinChan によって行われています。 これらのファイルを使用する前に、[Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt)を遵守することに加えて、アートコンテンツを使用するために前の 2 人の作者から許可を得る必要があります。
|
||||||
|
- 上記のファイルまたはディレクトリを除き、その他のすべての部分は[GPL-3.0 以降](https://www.gnu.org/licenses/gpl-3.0.html)です。
|
||||||
|
|
||||||
|
## スポンサーシップの一覧
|
||||||
|
|
||||||
|
- [Ktouls](https://github.com/Ktouls) 応援してくれてありがとう
|
||||||
|
- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトにご支援いただき、ありがとうございます
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) ありがとうございます
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) ラオスから 100 USDT の支援に感謝します
|
||||||
|
- [Saksham Singla](https://github.com/TypeFlu) ウェブサイトの提供とメンテナンス
|
||||||
|
- [OukaroMF](https://github.com/OukaroMF) ウェブサイトのドメインと寄付
|
||||||
|
|
||||||
|
## 貢献者
|
||||||
|
|
||||||
|
- [KernelSU](https://github.com/tiann/KernelSU): オリジナルのプロジェクト
|
||||||
|
- [MKSU](https://github.com/5ec1cff/KernelSU): 使用しているプロジェクト
|
||||||
|
- [RKSU](https://github.com/rsuntk/KernelsU): このプロジェクトのカーネルを使用した非 GKI デバイスのサポートの再導入
|
||||||
|
- [susfs](https://gitlab.com/simonpunk/susfs4ksu): susfs ファイルシステムの使用
|
||||||
|
- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU の概念化
|
||||||
|
- [Magisk](https://github.com/topjohnwu/Magisk): パワフルな root ユーティリティ
|
||||||
|
- [genuine](https://github.com/brevent/genuine/): APK v2 署名認証
|
||||||
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): いくつかの root キットユーティリティ
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch はカーネルモジュールの APatch 実装の重要な部分での活用
|
||||||
149
docs/tr/README.md
Normal file
149
docs/tr/README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# SukiSU Ultra
|
||||||
|
|
||||||
|
[English](../README.md) | [简体中文](../zh/README.md) | [日本語](../ja/README.md) | **Türkçe**
|
||||||
|
|
||||||
|
[KernelSU](https://github.com/tiann/KernelSU) tabanlı Android cihaz root çözümü
|
||||||
|
|
||||||
|
**Deneysel! Kullanım riski size aittir!**
|
||||||
|
|
||||||
|
> Bu resmi olmayan bir daldır, tüm hakları saklıdır [@tiann](https://github.com/tiann)
|
||||||
|
>
|
||||||
|
> Ancak, gelecekte ayrı bir KSU dalı olarak devam edeceğiz
|
||||||
|
|
||||||
|
## Nasıl Eklenir
|
||||||
|
|
||||||
|
Çekirdek kaynak kodunun kök dizininde aşağıdaki komutları çalıştırın:
|
||||||
|
|
||||||
|
Ana dalı kullanın (GKI olmayan cihazlar için desteklenmez)
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
GKI olmayan cihazları destekleyen dalı kullanın
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
|
## susfs Nasıl Entegre Edilir
|
||||||
|
|
||||||
|
1. Doğrudan susfs-main veya susfs-\* dalını kullanın, susfs entegrasyonuna gerek yok
|
||||||
|
|
||||||
|
```
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Kanca Yöntemleri
|
||||||
|
|
||||||
|
- Bu bölüm [rsuntk\'nin kanca yöntemlerinden](https://github.com/rsuntk/KernelSU) alıntılanmıştır
|
||||||
|
|
||||||
|
1. **KPROBES Kancası:**
|
||||||
|
|
||||||
|
- Yüklenebilir çekirdek modülleri (LKM) için kullanılır
|
||||||
|
- GKI 2.0 çekirdeğinin varsayılan kanca yöntemi
|
||||||
|
- `CONFIG_KPROBES=y` gerektirir
|
||||||
|
|
||||||
|
2. **Manuel Kanca:**
|
||||||
|
- Standart KernelSU kancası: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
|
||||||
|
- backslashxx\'nin syscall manuel kancası: https://github.com/backslashxx/KernelSU/issues/5
|
||||||
|
- GKI olmayan çekirdeğin varsayılan kanca yöntemi
|
||||||
|
- `CONFIG_KSU_MANUAL_HOOK=y` gerektirir
|
||||||
|
|
||||||
|
## KPM Desteği
|
||||||
|
|
||||||
|
- KernelPatch tabanlı olarak KSU ile çakışan işlevleri kaldırdık ve yalnızca KPM desteğini koruduk
|
||||||
|
- APatch ile daha fazla uyumlu fonksiyon ekleyerek KPM işlevlerinin bütünlüğünü sağlayacağız
|
||||||
|
|
||||||
|
Kaynak kodu: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
|
||||||
|
|
||||||
|
KPM şablonu: https://github.com/udochina/KPM-Build-Anywhere
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. `CONFIG_KPM=y` gerektirir
|
||||||
|
> 2. GKI olmayan cihazlar ayrıca `CONFIG_KALLSYMS=y` ve `CONFIG_KALLSYMS_ALL=y` gerektirir
|
||||||
|
> 3. Bazı çekirdek `4.19` altı kaynak kodları, `4.19`dan geri taşınan başlık dosyası `set_memory.h` gerektirir
|
||||||
|
|
||||||
|
## Sistem Güncellemesini Yaparak ROOT\'u Koruma
|
||||||
|
|
||||||
|
- OTA\'dan sonra hemen yeniden başlatmayın, yöneticiye girin ve çekirdek yazma/onarma arayüzüne gidin, `GKI/non_GKI yükleme` seçeneğini bulun ve Anykernel3 çekirdek sıkıştırma dosyasını seçin, şu anda sistemin çalıştığı yuva ile zıt yuvaya yazın ve yeniden başlatın, böylece GKI modu güncellemesini koruyabilirsiniz (şu anda tüm GKI olmayan cihazlar bu yöntemi desteklemiyor, lütfen kendiniz deneyin. GKI olmayan cihazlar için TWRP kullanmak en güvenlidir)
|
||||||
|
- Veya kullanılmayan yuvaya LKM modunu kullanarak yükleyin (OTA\'dan sonra)
|
||||||
|
|
||||||
|
## Uyumluluk Durumu
|
||||||
|
|
||||||
|
- KernelSU (v1.0.0 öncesi sürümler) resmi olarak Android GKI 2.0 cihazlarını destekler (çekirdek 5.10+)
|
||||||
|
|
||||||
|
- Eski çekirdekler (4.4+) de uyumludur, ancak çekirdeği manuel olarak oluşturmanız gerekir
|
||||||
|
|
||||||
|
- Daha fazla geri taşımayla KernelSU, 3.x çekirdeğini (3.4-3.18) destekleyebilir
|
||||||
|
|
||||||
|
- Şu anda `arm64-v8a`, `armeabi-v7a (bare)` ve bazı `X86_64` desteklenmektedir
|
||||||
|
|
||||||
|
## Daha Fazla Bağlantı
|
||||||
|
|
||||||
|
SukiSU ve susfs tabanlı derlenen projeler
|
||||||
|
|
||||||
|
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
|
||||||
|
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
|
||||||
|
|
||||||
|
## Kullanım Yöntemi
|
||||||
|
|
||||||
|
### Evrensel GKI
|
||||||
|
|
||||||
|
Lütfen **tümünü** https://kernelsu.org/zh_CN/guide/installation.html adresinden inceleyin
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. Xiaomi, Redmi, Samsung gibi GKI 2.0 cihazlar için uygundur (Meizu, OnePlus, Realme ve Oppo gibi değiştirilmiş çekirdekli üreticiler hariç)
|
||||||
|
> 2. [Daha fazla bağlantı](#daha-fazla-bağlantı) bölümündeki GKI tabanlı projeleri bulun. Cihaz çekirdek sürümünü bulun. Ardından indirin ve TWRP veya çekirdek yazma aracı kullanarak AnyKernel3 soneki olan sıkıştırılmış paketi yazın
|
||||||
|
> 3. Genellikle sonek olmayan .zip sıkıştırılmış paketler sıkıştırılmamıştır, gz soneki olanlar ise Dimensity modelleri için kullanılan sıkıştırma yöntemidir
|
||||||
|
|
||||||
|
### OnePlus
|
||||||
|
|
||||||
|
1. Daha fazla bağlantı bölümündeki OnePlus projesini bulun ve kendiniz doldurun, ardından bulut derleme yapın ve AnyKernel3 soneki olan sıkıştırılmış paketi yazın
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> - Çekirdek sürümü için yalnızca ilk iki haneyi doldurmanız yeterlidir, örneğin 5.10, 5.15, 6.1, 6.6
|
||||||
|
> - İşlemci kod adını kendiniz arayın, genellikle tamamen İngilizce ve sayı içermeden oluşur
|
||||||
|
> - Dal ve yapılandırma dosyasını kendiniz OnePlus çekirdek kaynak kodundan doldurun
|
||||||
|
|
||||||
|
## Özellikler
|
||||||
|
|
||||||
|
1. Çekirdek tabanlı `su` ve root erişim yönetimi
|
||||||
|
2. 5ec1cff\'nin [Magic Mount](https://github.com/5ec1cff/KernelSU) tabanlı modül sistemi
|
||||||
|
3. [App Profile](https://kernelsu.org/guide/app-profile.html): root yetkilerini kafeste kilitleyin
|
||||||
|
4. GKI 2.0 olmayan çekirdekler için desteğin geri getirilmesi
|
||||||
|
5. Daha fazla özelleştirme özelliği
|
||||||
|
6. KPM çekirdek modülleri için destek
|
||||||
|
|
||||||
|
## Lisans
|
||||||
|
|
||||||
|
- `kernel` dizinindeki dosyalar [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır.
|
||||||
|
- Anime karakter ifadeleri içeren `ic_launcher(?!.*alt.*).*` dosyalarının görüntüleri [怡子曰曰](https://space.bilibili.com/10545509) tarafından telif hakkıyla korunmaktadır, görüntülerdeki Marka Fikri Mülkiyeti [明风 OuO](https://space.bilibili.com/274939213)'ye aittir ve vektörleştirme @MiRinChan tarafından yapılmıştır. Bu dosyaları kullanmadan önce, [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) ile uyumlu olmanın yanı sıra, bu sanatsal içerikleri kullanmak için iki yazarın yetkilendirmesine de uymanız gerekir.
|
||||||
|
- Yukarıda belirtilen dosyalar veya dizinler hariç, diğer tüm parçalar [GPL-3.0 veya üzeri](https://www.gnu.org/licenses/gpl-3.0.html)'dir.
|
||||||
|
|
||||||
|
## Afdian Bağlantısı
|
||||||
|
|
||||||
|
- https://afdian.com/a/shirkneko
|
||||||
|
|
||||||
|
## Sponsor Listesi
|
||||||
|
|
||||||
|
- [Ktouls](https://github.com/Ktouls) Bana sağladığınız destek için çok teşekkür ederim
|
||||||
|
- [zaoqi123](https://github.com/zaoqi123) Bana sütlü çay ısmarlamanız da güzel
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) Bu projeye olan desteğiniz için çok teşekkür ederim
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) Çok teşekkür ederim
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) 100 USDT için teşekkürler
|
||||||
|
|
||||||
|
## Katkıda Bulunanlar
|
||||||
|
|
||||||
|
- [KernelSU](https://github.com/tiann/KernelSU): Orijinal proje
|
||||||
|
- [MKSU](https://github.com/5ec1cff/KernelSU): Kullanılan proje
|
||||||
|
- [RKSU](https://github.com/rsuntk/KernelsU): GKI olmayan cihazlar için destek sağlayan proje
|
||||||
|
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu): Kullanılan susfs dosya sistemi
|
||||||
|
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri
|
||||||
|
- [Magisk](https://github.com/topjohnwu/Magisk): Güçlü root aracı
|
||||||
|
- [genuine](https://github.com/brevent/genuine/): APK v2 imza doğrulama
|
||||||
|
- [Diamorphine](https://github.com/m0nad/Diamorphine): Bazı rootkit becerileri
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch, APatch\'in çekirdek modüllerini uygulamak için kritik bir parçadır
|
||||||
65
docs/zakomonochrome-128.svg
Normal file
65
docs/zakomonochrome-128.svg
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="zakomonochrome-128.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#999999"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="2.6185048"
|
||||||
|
inkscape:cx="59.957881"
|
||||||
|
inkscape:cy="71.032903"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
inkscape:window-height="696"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="图层 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.3;stroke-dasharray:none;paint-order:fill markers stroke;fill-opacity:1"
|
||||||
|
id="rect1"
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
rx="7.772471"
|
||||||
|
ry="7.772471" />
|
||||||
|
<path
|
||||||
|
id="path101"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.734285;stroke:#000000;stroke-width:4.27504;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.3;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
d="m 42.510282,81.796052 c 0,0 -7.224141,-5.638356 -10.043315,-9.338525 M 14.847106,81.97224 25.41902,71.576535 m 0.17619,-6.695549 2.819179,19.910444 M 11.675534,73.338532 38.281518,71.047931 M 43.567475,62.7666 34.40515,62.942814 M 34.22896,62.590425 33.524162,48.494537 m -18.500855,1.58577 17.972249,-1.409582 m -11.8053,-5.462154 0.352397,18.853251"
|
||||||
|
inkscape:label="杂" />
|
||||||
|
<path
|
||||||
|
id="path111"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.734285;stroke:#000000;stroke-width:3.94824;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.3;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
d="M 55.912937,82.876745 79.671596,81.412163 M 59.330273,75.391135 74.952411,74.089291 m -9.43837,-14.157553 1.139102,14.645756 m -8.299247,-7.160159 16.273048,-1.464569 m 0.650926,8.136525 0.325472,-14.808482 m -0.162747,0.162739 -17.900363,0.976379 m 0,-0.162738 1.952774,14.645756 m 12.042061,-21.154974 1.464576,-6.346492 m 0,-0.650928 -12.042063,0.650928 m -0.650918,6.509218 0.325459,-8.787441"
|
||||||
|
inkscape:label="鱼" />
|
||||||
|
<path
|
||||||
|
d="m 95.08569,51.121163 c -1.90515,0.116064 -3.64694,0.97349 -4.86738,2.391307 -1.34538,1.56738 -1.91476,3.733159 -1.59523,6.070852 0.40842,2.982962 2.1502,6.17135 5.13887,9.411078 0.63424,0.68546 1.08109,1.129773 1.98202,1.967071 1.58321,1.469144 3.01507,2.634638 4.9875,4.052454 0.70392,0.50905 2.09253,1.453525 2.61627,1.781734 l 0.15133,0.09594 0.22103,-0.140663 c 0.80481,-0.515755 2.23909,-1.504852 3.08956,-2.130057 3.21689,-2.364488 5.79232,-4.737902 7.70228,-7.100167 3.09676,-3.831409 4.4133,-7.562359 3.80549,-10.773058 -0.42043,-2.210414 -1.82588,-4.039057 -3.81992,-4.967887 -0.85767,-0.399664 -1.69132,-0.607312 -2.6355,-0.656431 -1.22285,-0.0647 -2.42648,0.178619 -3.57485,0.721182 -1.95561,0.922124 -3.58927,2.719503 -4.61752,5.081755 -0.072,0.165235 -0.1394,0.310355 -0.14895,0.319295 -0.0312,0.02902 -0.0648,-0.02679 -0.19458,-0.330457 -0.30752,-0.714476 -0.91055,-1.752718 -1.38382,-2.377871 -0.4853,-0.645282 -1.2661,-1.431214 -1.84749,-1.862143 -1.50155,-1.114153 -3.26013,-1.658924 -5.00914,-1.553996 z"
|
||||||
|
id="path1-4"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.00231605" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
101
docs/zh/README.md
Normal file
101
docs/zh/README.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<img align='right' src='zakomonochrome-128.svg' width='100px' alt="logo">
|
||||||
|
|
||||||
|
# SukiSU Ultra
|
||||||
|
|
||||||
|
[English](../README.md) | **简体中文** | [日本語](../ja/README.md) | [Türkçe](../tr/README.md)
|
||||||
|
|
||||||
|
一个 Android 上基于内核的 root 方案,由 [`tiann/KernelSU`](https://github.com/tiann/KernelSU) 分叉而来,添加了一些有趣的变更。
|
||||||
|
|
||||||
|
[](https://github.com/tiann/KernelSU/releases/latest)
|
||||||
|
[](https://t.me/Sukiksu)
|
||||||
|
[](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
|
||||||
|
[](/LICENSE)
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
1. 基于内核的 `su` 和权限管理。
|
||||||
|
2. 基于 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统。
|
||||||
|
3. [App Profile](https://kernelsu.org/zh_CN/guide/app-profile.html): 把 Root 权限关进笼子里。
|
||||||
|
4. 支持 non-GKI 与 GKI 1.0。
|
||||||
|
5. KPM 支持
|
||||||
|
6. 可调整管理器外观,可自定义 susfs 配置。
|
||||||
|
|
||||||
|
## 兼容状态
|
||||||
|
|
||||||
|
- KernelSU 官方支持 GKI 2.0 的设备(内核版本 5.10 以上)。
|
||||||
|
|
||||||
|
- 旧内核也是兼容的(最低 4.14+),不过需要自己编译内核。
|
||||||
|
|
||||||
|
- 通过更多的反向移植,KernelSU 可以支持 3.x 内核(3.4-3.18)。
|
||||||
|
|
||||||
|
- 目前支持架构 : `arm64-v8a`、`armeabi-v7a (bare)`、`X86_64`。
|
||||||
|
|
||||||
|
## 安装指导
|
||||||
|
|
||||||
|
查看 [`guide/installation.md`](guide/installation.md)
|
||||||
|
|
||||||
|
## 集成指导
|
||||||
|
|
||||||
|
查看 [`guide/how-to-integrate.md`](guide/how-to-integrate.md)
|
||||||
|
|
||||||
|
## 参与翻译
|
||||||
|
|
||||||
|
要将 SukiSU 翻译成您的语言,或完善现有的翻译,请使用 [Crowdin](https://crowdin.com/project/SukiSU-Ultra).
|
||||||
|
|
||||||
|
## KPM 支持
|
||||||
|
|
||||||
|
- 基于 KernelPatch 开发,移除了与 KernelSU 重复的功能。
|
||||||
|
- 正在进行(WIP):通过集成附加功能来扩展 APatch 兼容性,以确保跨不同实现的兼容性。
|
||||||
|
|
||||||
|
**开源仓库**: [https://github.com/ShirkNeko/SukiSU_KernelPatch_patch](https://github.com/ShirkNeko/SukiSU_KernelPatch_patch)
|
||||||
|
|
||||||
|
**KPM 模板**: [https://github.com/udochina/KPM-Build-Anywhere](https://github.com/udochina/KPM-Build-Anywhere)
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> 1. 需要 `CONFIG_KPM=y`
|
||||||
|
> 2. Non-GKI 设备需要 `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y`
|
||||||
|
> 3. 对于低于 `4.19` 的内核,需要从 `4.19` 的 `set_memory.h` 进行反向移植。
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
1. 卸载管理器后系统卡住?
|
||||||
|
卸载 _com.sony.playmemories.mobile_
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
- 目录 `kernel` 下所有文件为 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
|
||||||
|
- 有动漫人物图片表情包的这些文件 `ic_launcher(?!.*alt.*).*` 的图像版权为[怡子曰曰](https://space.bilibili.com/10545509)所有,图像中的知识产权由[明风 OuO](https://space.bilibili.com/274939213)所有,矢量化由 @MiRinChan 完成,在使用这些文件之前,除了必须遵守 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) 以外,还需要遵守向前两者索要使用这些艺术内容的授权。
|
||||||
|
- 除上述文件及目录的其他部分均为 [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。
|
||||||
|
|
||||||
|
## 赞助
|
||||||
|
|
||||||
|
- [ShirkNeko](https://afdian.com/a/shirkneko) (SukiSU 主要维护者)
|
||||||
|
- [weishu](https://github.com/sponsors/tiann) (KernelSU 作者)
|
||||||
|
|
||||||
|
## ShirkNeko 的赞助列表
|
||||||
|
|
||||||
|
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
|
||||||
|
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
|
||||||
|
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
|
||||||
|
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
|
||||||
|
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
|
||||||
|
- [Saksham Singla](https://github.com/TypeFlu) 网站的提供以及维护
|
||||||
|
- [OukaroMF](https://github.com/OukaroMF) 网站域名捐赠
|
||||||
|
|
||||||
|
## 鸣谢
|
||||||
|
|
||||||
|
- [KernelSU](https://github.com/tiann/KernelSU): 上游
|
||||||
|
- [MKSU](https://github.com/5ec1cff/KernelSU): 魔法坐骑支持
|
||||||
|
- [RKSU](https://github.com/rsuntk/KernelsU): non-GKI 支持
|
||||||
|
- [susfs](https://gitlab.com/simonpunk/susfs4ksu): 隐藏内核补丁以及用户空间模组的 KernelSU 附件
|
||||||
|
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是内核模块 APatch 实现的关键部分
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>KernelSU 的鸣谢</summary>
|
||||||
|
|
||||||
|
- [kernel-assisted-superuser](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 技巧。
|
||||||
|
</details>
|
||||||
97
docs/zh/guide/how-to-integrate.md
Normal file
97
docs/zh/guide/how-to-integrate.md
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
# 集成指导
|
||||||
|
|
||||||
|
SukiSU 可以集成到 GKI 和 non-GKI 内核中,并且已反向移植到 4.14 版本。
|
||||||
|
|
||||||
|
<!-- 应该是 3.4 版本,但 backslashxx 的 syscall manual hook 无法在 SukiSU 中使用-->
|
||||||
|
|
||||||
|
有些 OEM 定制可能导致多达 50% 的内核代码超出内核树代码,而非来自上游 Linux 内核或 ACK。因此,non-GKI 内核的定制特性导致了严重的内核碎片化,而且我们缺乏构建它们的通用方法。因此,我们无法提供 non-GKI 内核的启动映像。
|
||||||
|
|
||||||
|
前提条件:开源的、可启动的内核。
|
||||||
|
|
||||||
|
## Hook 方法
|
||||||
|
|
||||||
|
1. **KPROBES hook:**
|
||||||
|
|
||||||
|
- GKI kernels 的默认 hook 方法。
|
||||||
|
- 需要 `# CONFIG_KSU_MANUAL_HOOK is not set`(未设定) & `CONFIG_KPROBES=y`
|
||||||
|
- 用作可加载的内核模块 (LKM).
|
||||||
|
|
||||||
|
2. **Manual hook:**
|
||||||
|
|
||||||
|
<!-- - backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5 (v1.5 version is not available at the moment, if you want to use it, please use v1.4 version, or standard KernelSU hooks)-->
|
||||||
|
|
||||||
|
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
|
||||||
|
- 需要 [`guide/how-to-integrate.md`](how-to-integrate.md)
|
||||||
|
- 需要 [https://github.com/~](https://github.com/tiann/KernelSU/blob/main/website/docs/guide/how-to-integrate-for-non-gki.md#manually-modify-the-kernel-source)
|
||||||
|
|
||||||
|
3. **Tracepoint Hook:**
|
||||||
|
|
||||||
|
- 自 SukiSU commit [49b01aad](https://github.com/SukiSU-Ultra/SukiSU-Ultra/commit/49b01aad74bcca6dba5a8a2e053bb54b648eb124) 引入的 hook 方法
|
||||||
|
- 需要 `CONFIG_KSU_TRACEPOINT_HOOK=y`
|
||||||
|
- 需要 [`guide/tracepoint-hook.md`](tracepoint-hook.md)
|
||||||
|
|
||||||
|
<!-- This part refer to [rsuntk/KernelSU](https://github.com/rsuntk/KernelSU). -->
|
||||||
|
|
||||||
|
如果您能够构建可启动内核,有两种方法可以将 KernelSU 集成到内核源代码中:
|
||||||
|
|
||||||
|
1. 使用 `kprobe` 自动集成
|
||||||
|
2. 手动集成
|
||||||
|
|
||||||
|
## 与 kprobe 集成
|
||||||
|
|
||||||
|
适用:
|
||||||
|
|
||||||
|
- GKI 内核
|
||||||
|
|
||||||
|
不适用:
|
||||||
|
|
||||||
|
- non-GKI 内核
|
||||||
|
|
||||||
|
KernelSU 使用 kprobe 机制来做内核的相关 hook,如果 _kprobe_ 可以在你编译的内核中正常运行,那么推荐用这个方法来集成。
|
||||||
|
|
||||||
|
请参阅此文档 [https://github.com/~](https://github.com/tiann/KernelSU/blob/main/website/docs/guide/how-to-integrate-for-non-gki.md#integrate-with-kprobe)。虽然标题为“适用于 non-GKI”,但仅适用于 GKI。
|
||||||
|
|
||||||
|
替换 KernelSU 添加到内核源代码树的步骤的执行命令为:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
## 手动修改内核源代码
|
||||||
|
|
||||||
|
适用:
|
||||||
|
|
||||||
|
- GKI 内核
|
||||||
|
- non-GKI 内核
|
||||||
|
|
||||||
|
请参考此文档 [https://github.com/~ (non-GKI 内核集成)](https://github.com/tiann/KernelSU/blob/main/website/docs/guide/how-to-integrate-for-non-gki.md#manually-modify-the-kernel-source) 和 [https://github.com/~ (GKI 内核构建)](https://kernelsu.org/zh_CN/guide/how-to-build.html) 进行手动集成。虽然第一个链接的标题是“适用于 non-GKI”,但它也适用于 GKI。两者都可以正常工作。
|
||||||
|
|
||||||
|
还有另一种集成方法,但是仍在开发中。
|
||||||
|
|
||||||
|
<!-- 这是 backslashxx 的syscall manual hook,但目前无法使用。 -->
|
||||||
|
|
||||||
|
将 KernelSU(SukiSU)添加到内核源代码树的步骤的运行命令将被替换为:
|
||||||
|
|
||||||
|
### GKI 内核
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
|
||||||
|
```
|
||||||
|
|
||||||
|
### non-GKI 内核
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
|
||||||
|
```
|
||||||
|
|
||||||
|
### 带有 susfs 的 GKI / non-GKI 内核(实验)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-{{branch}}
|
||||||
|
```
|
||||||
|
|
||||||
|
分支:
|
||||||
|
|
||||||
|
- `main` (susfs-main)
|
||||||
|
- `test` (susfs-test)
|
||||||
|
- 版本号 (例如: susfs-1.5.7, 你需要在 [分支](https://github.com/SukiSU-Ultra/SukiSU-Ultra/branches) 里找到它)
|
||||||
34
docs/zh/guide/installation.md
Normal file
34
docs/zh/guide/installation.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 安装指导
|
||||||
|
|
||||||
|
您可以前往 [KernelSU 文档 - 安装](https://kernelsu.org/guide/installation.html) 获取有关如何安装的参考,这里只是额外的说明。
|
||||||
|
|
||||||
|
## 通过加载可加载内核模块 (LKM) 进行安装
|
||||||
|
|
||||||
|
请参阅 [KernelSU 文档 - LKM 安装](https://kernelsu.org/guide/installation.html#lkm-installation)
|
||||||
|
|
||||||
|
从 **Android™**(商标,意为获得 Google 移动服务的许可)12 开始,搭载内核版本 5.10 或更高版本的设备必须搭载 GKI 内核。因此你或许可以使用 LKM 模式。
|
||||||
|
|
||||||
|
## 通过安装内核进行安装
|
||||||
|
|
||||||
|
请参阅 [KernelSU 文档 - GKI 模式安装](https://kernelsu.org/guide/installation.html#gki-mode-installation)
|
||||||
|
|
||||||
|
我们提供预编译的内核供您使用:
|
||||||
|
|
||||||
|
- [ShirkNeko 内核](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)(添加了 ZRAM 压缩算法补丁、susfs 文件和 KPM 文件。适用于很多设备。)
|
||||||
|
- [MiRinFork 内核](https://github.com/MiRinFork/GKI_SukiSU_SUSFS)(添加了 susfs 文件和 KPM 文件。最接近 GKI 的内核,适用于大多数设备。)
|
||||||
|
|
||||||
|
虽然某些设备可以使用 LKM 模式安装,但无法使用 GKI 内核将其安装到设备上;因此,需要手动修改内核进行编译。例如:
|
||||||
|
|
||||||
|
- 欧珀(一加、真我)
|
||||||
|
- 魅族
|
||||||
|
|
||||||
|
此外,我们还为您的 OnePlus 设备提供预编译的内核:
|
||||||
|
|
||||||
|
- [ShirkNeko/Action_OnePlus_MKSU_SUSFS](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)(添加 ZRAM 压缩算法补丁、susfs 和 KPM。)
|
||||||
|
|
||||||
|
使用上面的链接,Fork 到 GitHub Action,填写构建参数,进行编译,最后将 zip 文件以 AnyKernel3 后缀上传到 GitHub Action。
|
||||||
|
|
||||||
|
> [!Note]
|
||||||
|
>
|
||||||
|
> - 使用时,您只需填写版本号的前两部分,例如 `5.10`、`6.1`...
|
||||||
|
> - 使用前请确保您了解处理器名称、内核版本等信息。
|
||||||
270
docs/zh/guide/tracepoint-hook.md
Normal file
270
docs/zh/guide/tracepoint-hook.md
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
# Tracepoint Hook 集成
|
||||||
|
|
||||||
|
## 介绍
|
||||||
|
|
||||||
|
自 commit [49b01aad](https://github.com/SukiSU-Ultra/SukiSU-Ultra/commit/49b01aad74bcca6dba5a8a2e053bb54b648eb124) 起,SukiSU 引入了 Tracepoint Hook
|
||||||
|
|
||||||
|
该 Hook 理论上相比于 Kprobes Hook,性能开销更小,但次于 Manual Hook / Syscall Hook
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 本教程参考了 [backslashxx/KernelSU#5](https://github.com/backslashxx/KernelSU/issues/5) 的 syscall hook v1.4 版本钩子,以及原版 KernelSU 的 [Manual Hook](https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source)
|
||||||
|
|
||||||
|
## Guide
|
||||||
|
|
||||||
|
### execve 钩子(`exec.c`)
|
||||||
|
|
||||||
|
一般需要修改 `fs/exec.c` 的 `do_execve` 和 `compat_do_execve` 方法
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/exec.c
|
||||||
|
+++ b/fs/exec.c
|
||||||
|
@@ -78,6 +78,10 @@
|
||||||
|
#include <trace/hooks/sched.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(task_rename);
|
||||||
|
|
||||||
|
static int bprm_creds_from_file(struct linux_binprm *bprm);
|
||||||
|
@@ -2037,6 +2041,9 @@ static int do_execve(struct filename *filename,
|
||||||
|
{
|
||||||
|
struct user_arg_ptr argv = { .ptr.native = __argv };
|
||||||
|
struct user_arg_ptr envp = { .ptr.native = __envp };
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_execveat_hook((int *)AT_FDCWD, &filename, &argv, &envp, 0);
|
||||||
|
+#endif
|
||||||
|
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -2064,6 +2071,9 @@ static int compat_do_execve(struct filename *filename,
|
||||||
|
.is_compat = true,
|
||||||
|
.ptr.compat = __envp,
|
||||||
|
};
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_execveat_sucompat_hook((int *)AT_FDCWD, &filename, NULL, NULL, NULL); /* 32-bit su */
|
||||||
|
+#endif
|
||||||
|
return do_execveat_common(AT_FDCWD, filename, argv, envp, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### faccessat 钩子 (`open.c`)
|
||||||
|
|
||||||
|
一般需要修改 `/fs/open.c` 的 `do_faccessat` 方法
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/open.c
|
||||||
|
+++ b/fs/open.c
|
||||||
|
@@ -37,6 +37,10 @@
|
||||||
|
#include "internal.h"
|
||||||
|
#include <trace/hooks/syscall_check.h>
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
int do_truncate(struct user_namespace *mnt_userns, struct dentry *dentry,
|
||||||
|
loff_t length, unsigned int time_attrs, struct file *filp)
|
||||||
|
{
|
||||||
|
@@ -468,6 +472,9 @@ static long do_faccessat(int dfd, const char __user *filename, int mode, int fla
|
||||||
|
|
||||||
|
SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)
|
||||||
|
{
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_faccessat_hook(&dfd, &filename, &mode, NULL);
|
||||||
|
+#endif
|
||||||
|
return do_faccessat(dfd, filename, mode, 0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果没有 `do_faccessat` 方法,可以找 `faccessat` 的 SYSCALL 定义(对于早于 4.17 的内核)
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/open.c
|
||||||
|
+++ b/fs/open.c
|
||||||
|
@@ -31,6 +31,9 @@
|
||||||
|
#include <linux/ima.h>
|
||||||
|
#include <linux/dnotify.h>
|
||||||
|
#include <linux/compat.h>
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
#include "internal.h"
|
||||||
|
|
||||||
|
@@ -369,6 +372,9 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)
|
||||||
|
int res;
|
||||||
|
unsigned int lookup_flags = LOOKUP_FOLLOW;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_faccessat_hook(&dfd, &filename, &mode, NULL);
|
||||||
|
+#endif
|
||||||
|
if (mode & ~S_IRWXO) /* where's F_OK, X_OK, W_OK, R_OK? */
|
||||||
|
return -EINVAL;
|
||||||
|
```
|
||||||
|
|
||||||
|
### sys_read 钩子 ( `read_write.c` )
|
||||||
|
|
||||||
|
需要修改 `fs/read_write.c` 的 `sys_read` 方法(4.19 及以上)
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/read_write.c
|
||||||
|
+++ b/fs/read_write.c
|
||||||
|
@@ -25,6 +25,10 @@
|
||||||
|
#include <linux/uaccess.h>
|
||||||
|
#include <asm/unistd.h>
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
const struct file_operations generic_ro_fops = {
|
||||||
|
.llseek = generic_file_llseek,
|
||||||
|
.read_iter = generic_file_read_iter,
|
||||||
|
@@ -630,6 +634,9 @@ ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
|
||||||
|
|
||||||
|
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
|
||||||
|
{
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_sys_read_hook(fd, &buf, &count);
|
||||||
|
+#endif
|
||||||
|
return ksys_read(fd, buf, count);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
或者是 `read` 的 SYSCALL 定义(4.14 及以下)
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/read_write.c
|
||||||
|
+++ b/fs/read_write.c
|
||||||
|
@@ -25,6 +25,11 @@
|
||||||
|
#include <linux/uaccess.h>
|
||||||
|
#include <asm/unistd.h>
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
+
|
||||||
|
const struct file_operations generic_ro_fops = {
|
||||||
|
.llseek = generic_file_llseek,
|
||||||
|
.read_iter = generic_file_read_iter,
|
||||||
|
@@ -575,6 +580,9 @@ SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
|
||||||
|
|
||||||
|
if (f.file) {
|
||||||
|
loff_t pos = file_pos_read(f.file);
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_sys_read_hook(fd, &buf, &count);
|
||||||
|
+#endif
|
||||||
|
ret = vfs_read(f.file, buf, count, &pos);
|
||||||
|
if (ret >= 0)
|
||||||
|
file_pos_write(f.file, pos);
|
||||||
|
```
|
||||||
|
|
||||||
|
### fstatat 钩子 ( `stat.c` )
|
||||||
|
|
||||||
|
需要修改 `stat.c` 的 `newfstatat` SYSCALL 定义
|
||||||
|
|
||||||
|
如果需要 32 位支持,还需要修改 `statat64` SYSCALL 定义
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/fs/stat.c
|
||||||
|
+++ b/fs/stat.c
|
||||||
|
@@ -24,6 +24,10 @@
|
||||||
|
#include "internal.h"
|
||||||
|
#include "mount.h"
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
/**
|
||||||
|
* generic_fillattr - Fill in the basic attributes from the inode struct
|
||||||
|
* @mnt_userns: user namespace of the mount the inode was found from
|
||||||
|
@@ -408,6 +412,10 @@ SYSCALL_DEFINE4(newfstatat, int, dfd, const char __user *, filename,
|
||||||
|
struct kstat stat;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_stat_hook(&dfd, &filename, &flag);
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
error = vfs_fstatat(dfd, filename, &stat, flag);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
@@ -559,6 +567,10 @@ SYSCALL_DEFINE4(fstatat64, int, dfd, const char __user *, filename,
|
||||||
|
struct kstat stat;
|
||||||
|
int error;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_stat_hook(&dfd, &filename, &flag); /* 32-bit su support */
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
error = vfs_fstatat(dfd, filename, &stat, flag);
|
||||||
|
if (error)
|
||||||
|
return error;
|
||||||
|
```
|
||||||
|
|
||||||
|
### input 钩子 (`input.c` ,用于进入KSU系的内置安全模式)
|
||||||
|
|
||||||
|
需要修改 `drivers/input/input.c` 的 `input_event` 方法,而不是 `input_handle_event`
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/drivers/input/input.c
|
||||||
|
+++ b/drivers/input/input.c
|
||||||
|
@@ -26,6 +26,10 @@
|
||||||
|
#include "input-compat.h"
|
||||||
|
#include "input-poller.h"
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
MODULE_AUTHOR("Vojtech Pavlik <vojtech@suse.cz>");
|
||||||
|
MODULE_DESCRIPTION("Input core");
|
||||||
|
MODULE_LICENSE("GPL");
|
||||||
|
@@ -451,6 +455,10 @@ void input_event(struct input_dev *dev,
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_input_hook(&type, &code, &value);
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
if (is_event_supported(type, dev->evbit, EV_MAX)) {
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dev->event_lock, flags);
|
||||||
|
```
|
||||||
|
|
||||||
|
### devpts 钩子 (`pty.c`)
|
||||||
|
|
||||||
|
需要修改 `drivers/tty/pty.c` 的 `pts_unix98_lookup` 方法
|
||||||
|
|
||||||
|
```patch
|
||||||
|
--- a/drivers/tty/pty.c
|
||||||
|
+++ b/drivers/tty/pty.c
|
||||||
|
@@ -31,6 +31,10 @@
|
||||||
|
#include <linux/compat.h>
|
||||||
|
#include "tty.h"
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+#include <../../drivers/kernelsu/ksu_trace.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
#undef TTY_DEBUG_HANGUP
|
||||||
|
#ifdef TTY_DEBUG_HANGUP
|
||||||
|
# define tty_debug_hangup(tty, f, args...) tty_debug(tty, f, ##args)
|
||||||
|
@@ -707,6 +711,10 @@ static struct tty_struct *pts_unix98_lookup(struct tty_driver *driver,
|
||||||
|
{
|
||||||
|
struct tty_struct *tty;
|
||||||
|
|
||||||
|
+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
+ trace_ksu_trace_devpts_hook((struct inode *)file->f_path.dentry->d_inode);
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
mutex_lock(&devpts_mutex);
|
||||||
|
tty = devpts_get_priv(file->f_path.dentry);
|
||||||
|
mutex_unlock(&devpts_mutex);
|
||||||
|
```
|
||||||
65
docs/zh/zakomonochrome-128.svg
Normal file
65
docs/zh/zakomonochrome-128.svg
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 128 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="zakomonochrome-128.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#999999"
|
||||||
|
borderopacity="1"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="2.6185048"
|
||||||
|
inkscape:cx="59.957881"
|
||||||
|
inkscape:cy="71.032903"
|
||||||
|
inkscape:window-width="1280"
|
||||||
|
inkscape:window-height="696"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<g
|
||||||
|
inkscape:label="图层 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<rect
|
||||||
|
style="fill:#ffffff;stroke:#000000;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.3;stroke-dasharray:none;paint-order:fill markers stroke;fill-opacity:1"
|
||||||
|
id="rect1"
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
rx="7.772471"
|
||||||
|
ry="7.772471" />
|
||||||
|
<path
|
||||||
|
id="path101"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.734285;stroke:#000000;stroke-width:4.27504;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.3;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
d="m 42.510282,81.796052 c 0,0 -7.224141,-5.638356 -10.043315,-9.338525 M 14.847106,81.97224 25.41902,71.576535 m 0.17619,-6.695549 2.819179,19.910444 M 11.675534,73.338532 38.281518,71.047931 M 43.567475,62.7666 34.40515,62.942814 M 34.22896,62.590425 33.524162,48.494537 m -18.500855,1.58577 17.972249,-1.409582 m -11.8053,-5.462154 0.352397,18.853251"
|
||||||
|
inkscape:label="杂" />
|
||||||
|
<path
|
||||||
|
id="path111"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.734285;stroke:#000000;stroke-width:3.94824;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.3;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
|
||||||
|
d="M 55.912937,82.876745 79.671596,81.412163 M 59.330273,75.391135 74.952411,74.089291 m -9.43837,-14.157553 1.139102,14.645756 m -8.299247,-7.160159 16.273048,-1.464569 m 0.650926,8.136525 0.325472,-14.808482 m -0.162747,0.162739 -17.900363,0.976379 m 0,-0.162738 1.952774,14.645756 m 12.042061,-21.154974 1.464576,-6.346492 m 0,-0.650928 -12.042063,0.650928 m -0.650918,6.509218 0.325459,-8.787441"
|
||||||
|
inkscape:label="鱼" />
|
||||||
|
<path
|
||||||
|
d="m 95.08569,51.121163 c -1.90515,0.116064 -3.64694,0.97349 -4.86738,2.391307 -1.34538,1.56738 -1.91476,3.733159 -1.59523,6.070852 0.40842,2.982962 2.1502,6.17135 5.13887,9.411078 0.63424,0.68546 1.08109,1.129773 1.98202,1.967071 1.58321,1.469144 3.01507,2.634638 4.9875,4.052454 0.70392,0.50905 2.09253,1.453525 2.61627,1.781734 l 0.15133,0.09594 0.22103,-0.140663 c 0.80481,-0.515755 2.23909,-1.504852 3.08956,-2.130057 3.21689,-2.364488 5.79232,-4.737902 7.70228,-7.100167 3.09676,-3.831409 4.4133,-7.562359 3.80549,-10.773058 -0.42043,-2.210414 -1.82588,-4.039057 -3.81992,-4.967887 -0.85767,-0.399664 -1.69132,-0.607312 -2.6355,-0.656431 -1.22285,-0.0647 -2.42648,0.178619 -3.57485,0.721182 -1.95561,0.922124 -3.58927,2.719503 -4.61752,5.081755 -0.072,0.165235 -0.1394,0.310355 -0.14895,0.319295 -0.0312,0.02902 -0.0648,-0.02679 -0.19458,-0.330457 -0.30752,-0.714476 -0.91055,-1.752718 -1.38382,-2.377871 -0.4853,-0.645282 -1.2661,-1.431214 -1.84749,-1.862143 -1.50155,-1.114153 -3.26013,-1.658924 -5.00914,-1.553996 z"
|
||||||
|
id="path1-4"
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.00231605" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -16,20 +16,42 @@ config KSU_DEBUG
|
|||||||
help
|
help
|
||||||
Enable KernelSU debug mode.
|
Enable KernelSU debug mode.
|
||||||
|
|
||||||
config KSU_HOOK
|
|
||||||
bool "Enable KernelSU Hook"
|
|
||||||
default n
|
|
||||||
help
|
|
||||||
This option enables the KernelSU Hook feature. If enabled, it will
|
|
||||||
override the kernel version check and enable the hook functionality.
|
|
||||||
|
|
||||||
config KPM
|
config KPM
|
||||||
bool "Enable SukiSU KPM"
|
bool "Enable SukiSU KPM"
|
||||||
|
depends on KSU && 64BIT
|
||||||
default n
|
default n
|
||||||
help
|
help
|
||||||
Enabling this option will activate the KPM feature of SukiSU.
|
Enabling this option will activate the KPM feature of SukiSU.
|
||||||
This option is suitable for scenarios where you need to force KPM to be enabled.
|
This option is suitable for scenarios where you need to force KPM to be enabled.
|
||||||
but it may affect system stability.
|
but it may affect system stability.
|
||||||
|
select KALLSYMS
|
||||||
|
select KALLSYMS_ALL
|
||||||
|
|
||||||
|
choice
|
||||||
|
prompt "KernelSU hook type"
|
||||||
|
depends on KSU
|
||||||
|
default KSU_KPROBES_HOOK
|
||||||
|
help
|
||||||
|
Hook type for KernelSU
|
||||||
|
|
||||||
|
config KSU_KPROBES_HOOK
|
||||||
|
bool "Hook KernelSU with Kprobes"
|
||||||
|
depends on KPROBES
|
||||||
|
help
|
||||||
|
If enabled, Hook required KernelSU syscalls with Kernel-probe.
|
||||||
|
|
||||||
|
config KSU_TRACEPOINT_HOOK
|
||||||
|
bool "Hook KernelSU with Tracepoint"
|
||||||
|
depends on TRACEPOINTS
|
||||||
|
help
|
||||||
|
If enabled, Hook required KernelSU syscalls with Tracepoint.
|
||||||
|
|
||||||
|
config KSU_MANUAL_HOOK
|
||||||
|
bool "Hook KernelSU manually"
|
||||||
|
depends on KSU != m
|
||||||
|
help
|
||||||
|
If enabled, Hook required KernelSU syscalls with manually-patched function.
|
||||||
|
|
||||||
|
endchoice
|
||||||
|
|
||||||
endmenu
|
endmenu
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
kernelsu-objs := ksu.o
|
kernelsu-objs := ksu.o
|
||||||
kernelsu-objs += allowlist.o
|
kernelsu-objs += allowlist.o
|
||||||
|
kernelsu-objs += dynamic_manager.o
|
||||||
kernelsu-objs += apk_sign.o
|
kernelsu-objs += apk_sign.o
|
||||||
kernelsu-objs += sucompat.o
|
kernelsu-objs += sucompat.o
|
||||||
kernelsu-objs += throne_tracker.o
|
kernelsu-objs += throne_tracker.o
|
||||||
@@ -8,6 +9,10 @@ kernelsu-objs += ksud.o
|
|||||||
kernelsu-objs += embed_ksud.o
|
kernelsu-objs += embed_ksud.o
|
||||||
kernelsu-objs += kernel_compat.o
|
kernelsu-objs += kernel_compat.o
|
||||||
|
|
||||||
|
ifeq ($(CONFIG_KSU_TRACEPOINT_HOOK), y)
|
||||||
|
kernelsu-objs += ksu_trace.o
|
||||||
|
endif
|
||||||
|
|
||||||
kernelsu-objs += selinux/selinux.o
|
kernelsu-objs += selinux/selinux.o
|
||||||
kernelsu-objs += selinux/sepolicy.o
|
kernelsu-objs += selinux/sepolicy.o
|
||||||
kernelsu-objs += selinux/rules.o
|
kernelsu-objs += selinux/rules.o
|
||||||
@@ -15,29 +20,71 @@ ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include
|
|||||||
ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h
|
ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h
|
||||||
|
|
||||||
obj-$(CONFIG_KSU) += kernelsu.o
|
obj-$(CONFIG_KSU) += kernelsu.o
|
||||||
|
obj-$(CONFIG_KSU_TRACEPOINT_HOOK) += ksu_trace_export.o
|
||||||
|
|
||||||
obj-$(CONFIG_KPM) += kpm/
|
obj-$(CONFIG_KPM) += kpm/
|
||||||
|
|
||||||
|
|
||||||
# .git is a text file while the module is imported by 'git submodule add'.
|
REPO_OWNER := SukiSU-Ultra
|
||||||
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
|
REPO_NAME := SukiSU-Ultra
|
||||||
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
|
REPO_BRANCH := main
|
||||||
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count main)
|
KSU_VERSION_API := 3.1.9
|
||||||
# ksu_version: major * 10000 + git version + 606 for historical reasons
|
|
||||||
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
|
GIT_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git
|
||||||
$(info -- KernelSU version: $(KSU_VERSION))
|
CURL_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin curl
|
||||||
|
|
||||||
|
KSU_GITHUB_VERSION := $(shell $(CURL_BIN) -s "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
|
||||||
|
KSU_GITHUB_VERSION_COMMIT := $(shell $(CURL_BIN) -sI "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/commits?sha=$(REPO_BRANCH)&per_page=1" | grep -i "link:" | sed -n 's/.*page=\([0-9]*\)>; rel="last".*/\1/p')
|
||||||
|
|
||||||
|
LOCAL_GIT_EXISTS := $(shell test -e $(srctree)/$(src)/../.git && echo 1 || echo 0)
|
||||||
|
|
||||||
|
define get_ksu_version_full
|
||||||
|
v$1-$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --abbrev-ref HEAD)
|
||||||
|
endef
|
||||||
|
|
||||||
|
ifeq ($(KSU_GITHUB_VERSION_COMMIT),)
|
||||||
|
ifeq ($(LOCAL_GIT_EXISTS),1)
|
||||||
|
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
|
||||||
|
KSU_LOCAL_VERSION := $(shell cd $(srctree)/$(src); $(GIT_BIN) rev-list --count $(REPO_BRANCH))
|
||||||
|
KSU_VERSION := $(shell expr 10000 + $(KSU_LOCAL_VERSION) + 700)
|
||||||
|
$(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION))
|
||||||
|
else
|
||||||
|
KSU_VERSION := 13000
|
||||||
|
$(warning -- Could not fetch version online or via local .git! Using fallback version: $(KSU_VERSION))
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
KSU_VERSION := $(shell expr 10000 + $(KSU_GITHUB_VERSION_COMMIT) + 700)
|
||||||
|
$(info -- $(REPO_NAME) version (GitHub): $(KSU_VERSION))
|
||||||
|
endif
|
||||||
|
|
||||||
|
ifeq ($(KSU_GITHUB_VERSION),)
|
||||||
|
ifeq ($(LOCAL_GIT_EXISTS),1)
|
||||||
|
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
|
||||||
|
KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_VERSION_API))
|
||||||
|
$(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION_FULL))
|
||||||
|
$(info -- $(REPO_NAME) Formatted version (local .git): $(KSU_VERSION))
|
||||||
|
else
|
||||||
|
KSU_VERSION_FULL := v$(KSU_VERSION_API)-$(REPO_NAME)-unknown@unknown
|
||||||
|
$(warning -- $(REPO_NAME) version: $(KSU_VERSION_FULL))
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
|
||||||
|
KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_GITHUB_VERSION))
|
||||||
|
$(info -- $(REPO_NAME) version (Github): $(KSU_VERSION_FULL))
|
||||||
|
endif
|
||||||
|
|
||||||
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
|
||||||
else # If there is no .git file, the default version will be passed.
|
ccflags-y += -DKSU_VERSION_FULL=\"$(KSU_VERSION_FULL)\"
|
||||||
$(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git submodule!")
|
|
||||||
ccflags-y += -DKSU_VERSION=16
|
# Custom Signs
|
||||||
|
ifdef KSU_EXPECTED_SIZE
|
||||||
|
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
||||||
|
$(info -- Custom KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifndef KSU_EXPECTED_SIZE
|
ifdef KSU_EXPECTED_HASH
|
||||||
KSU_EXPECTED_SIZE := 0x35c
|
ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\"
|
||||||
endif
|
$(info -- Custom KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
||||||
|
|
||||||
ifndef KSU_EXPECTED_HASH
|
|
||||||
KSU_EXPECTED_HASH := 947ae944f3de4ed4c21a7e4f7953ecf351bfa2b36239da37a34111ad29993eef
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifdef KSU_MANAGER_PACKAGE
|
ifdef KSU_MANAGER_PACKAGE
|
||||||
@@ -45,9 +92,16 @@ ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
|
|||||||
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
|
||||||
endif
|
endif
|
||||||
|
|
||||||
$(info -- SukiSU Manager signature size: $(KSU_EXPECTED_SIZE))
|
|
||||||
$(info -- SukiSU Manager signature hash: $(KSU_EXPECTED_HASH))
|
|
||||||
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
|
$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM))
|
||||||
|
|
||||||
|
ifeq ($(strip $(CONFIG_KSU_KPROBES_HOOK)),y)
|
||||||
|
$(info -- SukiSU: CONFIG_KSU_KPROBES_HOOK)
|
||||||
|
else ifeq ($(strip $(CONFIG_KSU_TRACEPOINT_HOOK)),y)
|
||||||
|
$(info -- SukiSU: CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
else ifeq ($(strip $(CONFIG_KSU_MANUAL_HOOK)),y)
|
||||||
|
$(info -- SukiSU: CONFIG_KSU_MANUAL_HOOK)
|
||||||
|
endif
|
||||||
|
|
||||||
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
|
||||||
KERNEL_TYPE := Non-GKI
|
KERNEL_TYPE := Non-GKI
|
||||||
# Check for GKI 2.0 (5.10+ or 6.x+)
|
# Check for GKI 2.0 (5.10+ or 6.x+)
|
||||||
@@ -69,10 +123,6 @@ else
|
|||||||
$(info -- KPM is disabled)
|
$(info -- KPM is disabled)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
|
||||||
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
|
|
||||||
ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\"
|
|
||||||
|
|
||||||
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
|
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
|
||||||
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function
|
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function
|
||||||
|
|
||||||
|
|||||||
@@ -15,15 +15,27 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "apk_sign.h"
|
#include "apk_sign.h"
|
||||||
|
#include "dynamic_manager.h"
|
||||||
#include "klog.h" // IWYU pragma: keep
|
#include "klog.h" // IWYU pragma: keep
|
||||||
#include "kernel_compat.h"
|
#include "kernel_compat.h"
|
||||||
|
#include "manager_sign.h"
|
||||||
|
|
||||||
struct sdesc {
|
struct sdesc {
|
||||||
struct shash_desc shash;
|
struct shash_desc shash;
|
||||||
char ctx[];
|
char ctx[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static struct apk_sign_key {
|
||||||
|
unsigned size;
|
||||||
|
const char *sha256;
|
||||||
|
} apk_sign_keys[] = {
|
||||||
|
{EXPECTED_SIZE_SHIRKNEKO, EXPECTED_HASH_SHIRKNEKO}, // ShirkNeko/SukiSU
|
||||||
|
{EXPECTED_SIZE_OTHER, EXPECTED_HASH_OTHER}, // Dynamic Sign
|
||||||
|
#ifdef EXPECTED_SIZE
|
||||||
|
{EXPECTED_SIZE, EXPECTED_HASH}, // Custom
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
static struct sdesc *init_sdesc(struct crypto_shash *alg)
|
static struct sdesc *init_sdesc(struct crypto_shash *alg)
|
||||||
{
|
{
|
||||||
struct sdesc *sdesc;
|
struct sdesc *sdesc;
|
||||||
@@ -70,10 +82,12 @@ static int ksu_sha256(const unsigned char *data, unsigned int datalen,
|
|||||||
crypto_free_shash(alg);
|
crypto_free_shash(alg);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, int *matched_index)
|
||||||
static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset,
|
|
||||||
unsigned expected_size, const char *expected_sha256)
|
|
||||||
{
|
{
|
||||||
|
int i;
|
||||||
|
struct apk_sign_key sign_key;
|
||||||
|
bool signature_valid = false;
|
||||||
|
|
||||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length
|
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length
|
||||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length
|
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length
|
||||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signed data length
|
ksu_kernel_read_compat(fp, size4, 0x4, pos); // signed data length
|
||||||
@@ -89,7 +103,20 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset,
|
|||||||
ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificate length
|
ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificate length
|
||||||
*offset += 0x4 * 2;
|
*offset += 0x4 * 2;
|
||||||
|
|
||||||
if (*size4 == expected_size) {
|
for (i = 0; i < ARRAY_SIZE(apk_sign_keys); i++) {
|
||||||
|
sign_key = apk_sign_keys[i];
|
||||||
|
|
||||||
|
if (i == 1) { // Dynamic Sign indexing
|
||||||
|
unsigned int size;
|
||||||
|
const char *hash;
|
||||||
|
if (ksu_get_dynamic_manager_config(&size, &hash)) {
|
||||||
|
sign_key.size = size;
|
||||||
|
sign_key.sha256 = hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (*size4 != sign_key.size)
|
||||||
|
continue;
|
||||||
*offset += *size4;
|
*offset += *size4;
|
||||||
|
|
||||||
#define CERT_MAX_LENGTH 1024
|
#define CERT_MAX_LENGTH 1024
|
||||||
@@ -109,13 +136,17 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset,
|
|||||||
hash_str[SHA256_DIGEST_SIZE * 2] = '\0';
|
hash_str[SHA256_DIGEST_SIZE * 2] = '\0';
|
||||||
|
|
||||||
bin2hex(hash_str, digest, SHA256_DIGEST_SIZE);
|
bin2hex(hash_str, digest, SHA256_DIGEST_SIZE);
|
||||||
pr_info("sha256: %s, expected: %s\n", hash_str,
|
pr_info("sha256: %s, expected: %s, index: %d\n", hash_str, sign_key.sha256, i);
|
||||||
expected_sha256);
|
|
||||||
if (strcmp(expected_sha256, hash_str) == 0) {
|
if (strcmp(sign_key.sha256, hash_str) == 0) {
|
||||||
return true;
|
signature_valid = true;
|
||||||
|
if (matched_index) {
|
||||||
|
*matched_index = i;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return signature_valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct zip_entry_header {
|
struct zip_entry_header {
|
||||||
@@ -155,8 +186,7 @@ static bool has_v1_signature_file(struct file *fp)
|
|||||||
fileName[header.file_name_length] = '\0';
|
fileName[header.file_name_length] = '\0';
|
||||||
|
|
||||||
// Check if the entry matches META-INF/MANIFEST.MF
|
// Check if the entry matches META-INF/MANIFEST.MF
|
||||||
if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) ==
|
if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == 0) {
|
||||||
0) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -171,21 +201,17 @@ static bool has_v1_signature_file(struct file *fp)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static __always_inline bool check_v2_signature(char *path,
|
static __always_inline bool check_v2_signature(char *path, bool check_multi_manager, int *signature_index)
|
||||||
unsigned expected_size,
|
|
||||||
const char *expected_sha256)
|
|
||||||
{
|
{
|
||||||
unsigned char buffer[0x11] = { 0 };
|
unsigned char buffer[0x11] = { 0 };
|
||||||
u32 size4;
|
u32 size4;
|
||||||
u64 size8, size_of_block;
|
u64 size8, size_of_block;
|
||||||
|
|
||||||
loff_t pos;
|
loff_t pos;
|
||||||
|
|
||||||
bool v2_signing_valid = false;
|
bool v2_signing_valid = false;
|
||||||
int v2_signing_blocks = 0;
|
int v2_signing_blocks = 0;
|
||||||
bool v3_signing_exist = false;
|
bool v3_signing_exist = false;
|
||||||
bool v3_1_signing_exist = false;
|
bool v3_1_signing_exist = false;
|
||||||
|
int matched_index = -1;
|
||||||
int i;
|
int i;
|
||||||
struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0);
|
struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0);
|
||||||
if (IS_ERR(fp)) {
|
if (IS_ERR(fp)) {
|
||||||
@@ -193,6 +219,12 @@ static __always_inline bool check_v2_signature(char *path,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If you want to check for multi-manager APK signing, but dynamic managering is not enabled, skip
|
||||||
|
if (check_multi_manager && !ksu_is_dynamic_manager_enabled()) {
|
||||||
|
filp_close(fp, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// disable inotify for this file
|
// disable inotify for this file
|
||||||
fp->f_mode |= FMODE_NONOTIFY;
|
fp->f_mode |= FMODE_NONOTIFY;
|
||||||
|
|
||||||
@@ -244,9 +276,10 @@ static __always_inline bool check_v2_signature(char *path,
|
|||||||
offset = 4;
|
offset = 4;
|
||||||
if (id == 0x7109871au) {
|
if (id == 0x7109871au) {
|
||||||
v2_signing_blocks++;
|
v2_signing_blocks++;
|
||||||
v2_signing_valid =
|
bool result = check_block(fp, &size4, &pos, &offset, &matched_index);
|
||||||
check_block(fp, &size4, &pos, &offset,
|
if (result) {
|
||||||
expected_size, expected_sha256);
|
v2_signing_valid = true;
|
||||||
|
}
|
||||||
} else if (id == 0xf05368c0u) {
|
} else if (id == 0xf05368c0u) {
|
||||||
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73
|
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73
|
||||||
v3_signing_exist = true;
|
v3_signing_exist = true;
|
||||||
@@ -287,7 +320,24 @@ clean:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return v2_signing_valid;
|
if (v2_signing_valid) {
|
||||||
|
if (signature_index) {
|
||||||
|
*signature_index = matched_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check_multi_manager) {
|
||||||
|
// 0: ShirkNeko/SukiSU, 1: Dynamic Sign
|
||||||
|
if (matched_index == 0 || matched_index == 1) {
|
||||||
|
pr_info("Multi-manager APK detected (dynamic_manager enabled): signature_index=%d\n", matched_index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
// Common manager check: any valid signature will do
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CONFIG_KSU_DEBUG
|
#ifdef CONFIG_KSU_DEBUG
|
||||||
@@ -316,5 +366,10 @@ module_param_cb(ksu_debug_manager_uid, &expected_size_ops,
|
|||||||
|
|
||||||
bool is_manager_apk(char *path)
|
bool is_manager_apk(char *path)
|
||||||
{
|
{
|
||||||
return check_v2_signature(path, EXPECTED_SIZE, EXPECTED_HASH);
|
return check_v2_signature(path, false, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ksu_is_dynamic_manager_apk(char *path, int *signature_index)
|
||||||
|
{
|
||||||
|
return check_v2_signature(path, true, signature_index);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#define __KSU_H_APK_V2_SIGN
|
#define __KSU_H_APK_V2_SIGN
|
||||||
|
|
||||||
#include <linux/types.h>
|
#include <linux/types.h>
|
||||||
|
#include "ksu.h"
|
||||||
|
|
||||||
bool is_manager_apk(char *path);
|
bool is_manager_apk(char *path);
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,10 @@
|
|||||||
#include "manager.h"
|
#include "manager.h"
|
||||||
#include "selinux/selinux.h"
|
#include "selinux/selinux.h"
|
||||||
#include "throne_tracker.h"
|
#include "throne_tracker.h"
|
||||||
#include "throne_tracker.h"
|
|
||||||
#include "kernel_compat.h"
|
#include "kernel_compat.h"
|
||||||
|
|
||||||
#include "kpm/kpm.h"
|
#include "kpm/kpm.h"
|
||||||
|
#include "dynamic_manager.h"
|
||||||
|
|
||||||
static bool ksu_module_mounted = false;
|
static bool ksu_module_mounted = false;
|
||||||
|
|
||||||
@@ -110,6 +110,7 @@ static void setup_groups(struct root_profile *profile, struct cred *cred)
|
|||||||
|
|
||||||
groups_sort(group_info);
|
groups_sort(group_info);
|
||||||
set_groups(cred, group_info);
|
set_groups(cred, group_info);
|
||||||
|
put_group_info(group_info);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void disable_seccomp()
|
static void disable_seccomp()
|
||||||
@@ -134,18 +135,18 @@ void escape_to_root(void)
|
|||||||
{
|
{
|
||||||
struct cred *cred;
|
struct cred *cred;
|
||||||
|
|
||||||
rcu_read_lock();
|
cred = prepare_creds();
|
||||||
|
if (!cred) {
|
||||||
do {
|
pr_warn("prepare_creds failed!\n");
|
||||||
cred = (struct cred *)__task_cred((current));
|
return;
|
||||||
BUG_ON(!cred);
|
}
|
||||||
} while (!get_cred_rcu(cred));
|
|
||||||
|
|
||||||
if (cred->euid.val == 0) {
|
if (cred->euid.val == 0) {
|
||||||
pr_warn("Already root, don't escape!\n");
|
pr_warn("Already root, don't escape!\n");
|
||||||
rcu_read_unlock();
|
abort_creds(cred);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct root_profile *profile = ksu_get_root_profile(cred->uid.val);
|
struct root_profile *profile = ksu_get_root_profile(cred->uid.val);
|
||||||
|
|
||||||
cred->uid.val = profile->uid;
|
cred->uid.val = profile->uid;
|
||||||
@@ -176,7 +177,7 @@ void escape_to_root(void)
|
|||||||
|
|
||||||
setup_groups(profile, cred);
|
setup_groups(profile, cred);
|
||||||
|
|
||||||
rcu_read_unlock();
|
commit_creds(cred);
|
||||||
|
|
||||||
// Refer to kernel/seccomp.c: seccomp_set_mode_strict
|
// Refer to kernel/seccomp.c: seccomp_set_mode_strict
|
||||||
// When disabling Seccomp, ensure that current->sighand->siglock is held during the operation.
|
// When disabling Seccomp, ensure that current->sighand->siglock is held during the operation.
|
||||||
@@ -226,6 +227,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_EXT4_FS
|
||||||
static void nuke_ext4_sysfs() {
|
static void nuke_ext4_sysfs() {
|
||||||
struct path path;
|
struct path path;
|
||||||
int err = kern_path("/data/adb/modules", 0, &path);
|
int err = kern_path("/data/adb/modules", 0, &path);
|
||||||
@@ -242,6 +244,20 @@ static void nuke_ext4_sysfs() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext4_unregister_sysfs(sb);
|
ext4_unregister_sysfs(sb);
|
||||||
|
path_put(&path);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static inline void nuke_ext4_sysfs() { }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static bool is_system_bin_su()
|
||||||
|
{
|
||||||
|
// YES in_execve becomes 0 when it succeeds.
|
||||||
|
if (!current->mm || current->in_execve)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// quick af check
|
||||||
|
return (current->mm->exe_file && !strcmp(current->mm->exe_file->f_path.dentry->d_name.name, "su"));
|
||||||
}
|
}
|
||||||
|
|
||||||
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
||||||
@@ -266,7 +282,8 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
bool from_root = 0 == current_uid().val;
|
bool from_root = 0 == current_uid().val;
|
||||||
bool from_manager = is_manager();
|
bool from_manager = is_manager();
|
||||||
|
|
||||||
if (!from_root && !from_manager) {
|
if (!from_root && !from_manager
|
||||||
|
&& !(is_allow_su() && is_system_bin_su())) {
|
||||||
// only root or manager can access this interface
|
// only root or manager can access this interface
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -302,7 +319,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
if (copy_to_user(arg3, &version, sizeof(version))) {
|
if (copy_to_user(arg3, &version, sizeof(version))) {
|
||||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||||
}
|
}
|
||||||
u32 version_flags = 0;
|
u32 version_flags = 2;
|
||||||
#ifdef MODULE
|
#ifdef MODULE
|
||||||
version_flags |= 0x1;
|
version_flags |= 0x1;
|
||||||
#endif
|
#endif
|
||||||
@@ -313,6 +330,72 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow root manager to get full version strings
|
||||||
|
if (arg2 == CMD_GET_FULL_VERSION) {
|
||||||
|
char ksu_version_full[KSU_FULL_VERSION_STRING] = {0};
|
||||||
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
|
||||||
|
strscpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING);
|
||||||
|
#else
|
||||||
|
strlcpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING);
|
||||||
|
#endif
|
||||||
|
if (copy_to_user((void __user *)arg3, ksu_version_full, KSU_FULL_VERSION_STRING)) {
|
||||||
|
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||||
|
return -EFAULT;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the root manager to configure dynamic manageratures
|
||||||
|
if (arg2 == CMD_DYNAMIC_MANAGER) {
|
||||||
|
if (!from_root && !from_manager) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dynamic_manager_user_config config;
|
||||||
|
|
||||||
|
if (copy_from_user(&config, (void __user *)arg3, sizeof(config))) {
|
||||||
|
pr_err("copy dynamic manager config failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = ksu_handle_dynamic_manager(&config);
|
||||||
|
|
||||||
|
if (ret == 0 && config.operation == DYNAMIC_MANAGER_OP_GET) {
|
||||||
|
if (copy_to_user((void __user *)arg3, &config, sizeof(config))) {
|
||||||
|
pr_err("copy dynamic manager config back failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret == 0) {
|
||||||
|
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||||
|
pr_err("dynamic_manager: prctl reply error\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow root manager to get active managers
|
||||||
|
if (arg2 == CMD_GET_MANAGERS) {
|
||||||
|
if (!from_root && !from_manager) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct manager_list_info manager_info;
|
||||||
|
int ret = ksu_get_active_managers(&manager_info);
|
||||||
|
|
||||||
|
if (ret == 0) {
|
||||||
|
if (copy_to_user((void __user *)arg3, &manager_info, sizeof(manager_info))) {
|
||||||
|
pr_err("copy manager list failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||||
|
pr_err("get_managers: prctl reply error\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (arg2 == CMD_REPORT_EVENT) {
|
if (arg2 == CMD_REPORT_EVENT) {
|
||||||
if (!from_root) {
|
if (!from_root) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -324,6 +407,9 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
post_fs_data_lock = true;
|
post_fs_data_lock = true;
|
||||||
pr_info("post-fs-data triggered\n");
|
pr_info("post-fs-data triggered\n");
|
||||||
on_post_fs_data();
|
on_post_fs_data();
|
||||||
|
// Initializing Dynamic Signatures
|
||||||
|
ksu_dynamic_manager_init();
|
||||||
|
pr_info("Dynamic sign config loaded during post-fs-data\n");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -412,6 +498,30 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (arg2 == CMD_ENABLE_SU) {
|
||||||
|
bool enabled = (arg3 != 0);
|
||||||
|
if (enabled == ksu_su_compat_enabled) {
|
||||||
|
pr_info("cmd enable su but no need to change.\n");
|
||||||
|
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {// return the reply_ok directly
|
||||||
|
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
ksu_sucompat_init();
|
||||||
|
} else {
|
||||||
|
ksu_sucompat_exit();
|
||||||
|
}
|
||||||
|
ksu_su_compat_enabled = enabled;
|
||||||
|
|
||||||
|
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||||
|
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef CONFIG_KPM
|
#ifdef CONFIG_KPM
|
||||||
// ADD: 添加KPM模块控制
|
// ADD: 添加KPM模块控制
|
||||||
if(sukisu_is_kpm_control_code(arg2)) {
|
if(sukisu_is_kpm_control_code(arg2)) {
|
||||||
@@ -425,6 +535,35 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (arg2 == CMD_ENABLE_KPM) {
|
||||||
|
bool KPM_Enabled = IS_ENABLED(CONFIG_KPM);
|
||||||
|
if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled)))
|
||||||
|
pr_info("KPM: copy_to_user() failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checking hook usage
|
||||||
|
if (arg2 == CMD_HOOK_TYPE) {
|
||||||
|
const char *hook_type = "Kprobes";
|
||||||
|
#if defined(CONFIG_KSU_TRACEPOINT_HOOK)
|
||||||
|
hook_type = "Tracepoint";
|
||||||
|
#elif defined(CONFIG_KSU_MANUAL_HOOK)
|
||||||
|
hook_type = "Manual";
|
||||||
|
#endif
|
||||||
|
|
||||||
|
size_t len = strlen(hook_type) + 1;
|
||||||
|
if (copy_to_user((void __user *)arg3, hook_type, len)) {
|
||||||
|
pr_err("hook_type: copy_to_user failed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
||||||
|
pr_err("hook_type: prctl reply error\n");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// all other cmds are for 'root manager'
|
// all other cmds are for 'root manager'
|
||||||
if (!from_manager) {
|
if (!from_manager) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -479,30 +618,6 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg2 == CMD_ENABLE_SU) {
|
|
||||||
bool enabled = (arg3 != 0);
|
|
||||||
if (enabled == ksu_su_compat_enabled) {
|
|
||||||
pr_info("cmd enable su but no need to change.\n");
|
|
||||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {// return the reply_ok directly
|
|
||||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enabled) {
|
|
||||||
ksu_sucompat_init();
|
|
||||||
} else {
|
|
||||||
ksu_sucompat_exit();
|
|
||||||
}
|
|
||||||
ksu_su_compat_enabled = enabled;
|
|
||||||
|
|
||||||
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
|
|
||||||
pr_err("prctl reply error, cmd: %lu\n", arg2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,11 +668,13 @@ static void try_umount(const char *mnt, bool check_mnt, int flags)
|
|||||||
|
|
||||||
if (path.dentry != path.mnt->mnt_root) {
|
if (path.dentry != path.mnt->mnt_root) {
|
||||||
// it is not root mountpoint, maybe umounted by others already.
|
// it is not root mountpoint, maybe umounted by others already.
|
||||||
|
path_put(&path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// we are only interest in some specific mounts
|
// we are only interest in some specific mounts
|
||||||
if (check_mnt && !should_umount(&path)) {
|
if (check_mnt && !should_umount(&path)) {
|
||||||
|
path_put(&path);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -713,6 +830,19 @@ static int ksu_task_fix_setuid(struct cred *new, const struct cred *old,
|
|||||||
}
|
}
|
||||||
|
|
||||||
#ifndef MODULE
|
#ifndef MODULE
|
||||||
|
|
||||||
|
extern int __ksu_handle_devpts(struct inode *inode);
|
||||||
|
static int ksu_inode_permission(struct inode *inode, int mask)
|
||||||
|
{
|
||||||
|
if (unlikely(inode->i_sb && inode->i_sb->s_magic == DEVPTS_SUPER_MAGIC)) {
|
||||||
|
#ifdef CONFIG_KSU_DEBUG
|
||||||
|
pr_info("%s: devpts inode accessed with mask: %x\n", __func__, mask);
|
||||||
|
#endif
|
||||||
|
__ksu_handle_devpts(inode);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static struct security_hook_list ksu_hooks[] = {
|
static struct security_hook_list ksu_hooks[] = {
|
||||||
LSM_HOOK_INIT(task_prctl, ksu_task_prctl),
|
LSM_HOOK_INIT(task_prctl, ksu_task_prctl),
|
||||||
LSM_HOOK_INIT(inode_rename, ksu_inode_rename),
|
LSM_HOOK_INIT(inode_rename, ksu_inode_rename),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define __KSU_H_KSU_CORE
|
#define __KSU_H_KSU_CORE
|
||||||
|
|
||||||
#include <linux/init.h>
|
#include <linux/init.h>
|
||||||
|
#include "apk_sign.h"
|
||||||
|
|
||||||
void __init ksu_core_init(void);
|
void __init ksu_core_init(void);
|
||||||
void ksu_core_exit(void);
|
void ksu_core_exit(void);
|
||||||
|
|||||||
505
kernel/dynamic_manager.c
Normal file
505
kernel/dynamic_manager.c
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
#include <linux/err.h>
|
||||||
|
#include <linux/fs.h>
|
||||||
|
#include <linux/gfp.h>
|
||||||
|
#include <linux/kernel.h>
|
||||||
|
#include <linux/slab.h>
|
||||||
|
#include <linux/version.h>
|
||||||
|
#include <linux/workqueue.h>
|
||||||
|
#ifdef CONFIG_KSU_DEBUG
|
||||||
|
#include <linux/moduleparam.h>
|
||||||
|
#endif
|
||||||
|
#include <crypto/hash.h>
|
||||||
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)
|
||||||
|
#include <crypto/sha2.h>
|
||||||
|
#else
|
||||||
|
#include <crypto/sha.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "dynamic_manager.h"
|
||||||
|
#include "klog.h" // IWYU pragma: keep
|
||||||
|
#include "kernel_compat.h"
|
||||||
|
#include "manager.h"
|
||||||
|
|
||||||
|
#define MAX_MANAGERS 2
|
||||||
|
|
||||||
|
// Dynamic sign configuration
|
||||||
|
static struct dynamic_manager_config dynamic_manager = {
|
||||||
|
.size = 0x300,
|
||||||
|
.hash = "0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
.is_set = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Multi-manager state
|
||||||
|
static struct manager_info active_managers[MAX_MANAGERS];
|
||||||
|
static DEFINE_SPINLOCK(managers_lock);
|
||||||
|
static DEFINE_SPINLOCK(dynamic_manager_lock);
|
||||||
|
|
||||||
|
// Work queues for persistent storage
|
||||||
|
static struct work_struct ksu_save_dynamic_manager_work;
|
||||||
|
static struct work_struct ksu_load_dynamic_manager_work;
|
||||||
|
static struct work_struct ksu_clear_dynamic_manager_work;
|
||||||
|
|
||||||
|
bool ksu_is_dynamic_manager_enabled(void)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
bool enabled;
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
enabled = dynamic_manager.is_set;
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_add_manager(uid_t uid, int signature_index)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!ksu_is_dynamic_manager_enabled()) {
|
||||||
|
pr_info("Dynamic sign not enabled, skipping multi-manager add\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_lock_irqsave(&managers_lock, flags);
|
||||||
|
|
||||||
|
// Check if manager already exists and update
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
if (active_managers[i].is_active && active_managers[i].uid == uid) {
|
||||||
|
active_managers[i].signature_index = signature_index;
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
pr_info("Updated manager uid=%d, signature_index=%d\n", uid, signature_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find free slot for new manager
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
if (!active_managers[i].is_active) {
|
||||||
|
active_managers[i].uid = uid;
|
||||||
|
active_managers[i].signature_index = signature_index;
|
||||||
|
active_managers[i].is_active = true;
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
pr_info("Added manager uid=%d, signature_index=%d\n", uid, signature_index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
pr_warn("Failed to add manager, no free slots\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_remove_manager(uid_t uid)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!ksu_is_dynamic_manager_enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_lock_irqsave(&managers_lock, flags);
|
||||||
|
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
if (active_managers[i].is_active && active_managers[i].uid == uid) {
|
||||||
|
active_managers[i].is_active = false;
|
||||||
|
pr_info("Removed manager uid=%d\n", uid);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ksu_is_any_manager(uid_t uid)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
bool is_manager = false;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!ksu_is_dynamic_manager_enabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_lock_irqsave(&managers_lock, flags);
|
||||||
|
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
if (active_managers[i].is_active && active_managers[i].uid == uid) {
|
||||||
|
is_manager = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
return is_manager;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ksu_get_manager_signature_index(uid_t uid)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
int signature_index = -1;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
// Check traditional manager first
|
||||||
|
if (ksu_manager_uid != KSU_INVALID_UID && uid == ksu_manager_uid) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ksu_is_dynamic_manager_enabled()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_lock_irqsave(&managers_lock, flags);
|
||||||
|
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
if (active_managers[i].is_active && active_managers[i].uid == uid) {
|
||||||
|
signature_index = active_managers[i].signature_index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
return signature_index;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_dynamic_manager(void)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
spin_lock_irqsave(&managers_lock, flags);
|
||||||
|
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
if (active_managers[i].is_active) {
|
||||||
|
pr_info("Clearing dynamic manager uid=%d (signature_index=%d) for rescan\n",
|
||||||
|
active_managers[i].uid, active_managers[i].signature_index);
|
||||||
|
active_managers[i].is_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ksu_get_active_managers(struct manager_list_info *info)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
int i, count = 0;
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add traditional manager first
|
||||||
|
if (ksu_manager_uid != KSU_INVALID_UID && count < 2) {
|
||||||
|
info->managers[count].uid = ksu_manager_uid;
|
||||||
|
info->managers[count].signature_index = 0;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add dynamic managers
|
||||||
|
if (ksu_is_dynamic_manager_enabled()) {
|
||||||
|
spin_lock_irqsave(&managers_lock, flags);
|
||||||
|
|
||||||
|
for (i = 0; i < MAX_MANAGERS && count < 2; i++) {
|
||||||
|
if (active_managers[i].is_active) {
|
||||||
|
info->managers[count].uid = active_managers[i].uid;
|
||||||
|
info->managers[count].signature_index = active_managers[i].signature_index;
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_unlock_irqrestore(&managers_lock, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
info->count = count;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void do_save_dynamic_manager(struct work_struct *work)
|
||||||
|
{
|
||||||
|
u32 magic = DYNAMIC_MANAGER_FILE_MAGIC;
|
||||||
|
u32 version = DYNAMIC_MANAGER_FILE_VERSION;
|
||||||
|
struct dynamic_manager_config config_to_save;
|
||||||
|
loff_t off = 0;
|
||||||
|
unsigned long flags;
|
||||||
|
struct file *fp;
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
config_to_save = dynamic_manager;
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
|
||||||
|
if (!config_to_save.is_set) {
|
||||||
|
pr_info("Dynamic sign config not set, skipping save\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (IS_ERR(fp)) {
|
||||||
|
pr_err("save_dynamic_manager create file failed: %ld\n", PTR_ERR(fp));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic)) {
|
||||||
|
pr_err("save_dynamic_manager write magic failed.\n");
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != sizeof(version)) {
|
||||||
|
pr_err("save_dynamic_manager write version failed.\n");
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ksu_kernel_write_compat(fp, &config_to_save, sizeof(config_to_save), &off) != sizeof(config_to_save)) {
|
||||||
|
pr_err("save_dynamic_manager write config failed.\n");
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pr_info("Dynamic sign config saved successfully\n");
|
||||||
|
|
||||||
|
exit:
|
||||||
|
filp_close(fp, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void do_load_dynamic_manager(struct work_struct *work)
|
||||||
|
{
|
||||||
|
loff_t off = 0;
|
||||||
|
ssize_t ret = 0;
|
||||||
|
struct file *fp = NULL;
|
||||||
|
u32 magic;
|
||||||
|
u32 version;
|
||||||
|
struct dynamic_manager_config loaded_config;
|
||||||
|
unsigned long flags;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_RDONLY, 0);
|
||||||
|
if (IS_ERR(fp)) {
|
||||||
|
if (PTR_ERR(fp) == -ENOENT) {
|
||||||
|
pr_info("No saved dynamic manager config found\n");
|
||||||
|
} else {
|
||||||
|
pr_err("load_dynamic_manager open file failed: %ld\n", PTR_ERR(fp));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic) ||
|
||||||
|
magic != DYNAMIC_MANAGER_FILE_MAGIC) {
|
||||||
|
pr_err("dynamic manager file invalid magic: %x!\n", magic);
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != sizeof(version)) {
|
||||||
|
pr_err("dynamic manager read version failed\n");
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
pr_info("dynamic manager file version: %d\n", version);
|
||||||
|
|
||||||
|
ret = ksu_kernel_read_compat(fp, &loaded_config, sizeof(loaded_config), &off);
|
||||||
|
if (ret <= 0) {
|
||||||
|
pr_info("load_dynamic_manager read err: %zd\n", ret);
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret != sizeof(loaded_config)) {
|
||||||
|
pr_err("load_dynamic_manager read incomplete config: %zd/%zu\n", ret, sizeof(loaded_config));
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loaded_config.size < 0x100 || loaded_config.size > 0x1000) {
|
||||||
|
pr_err("Invalid saved config size: 0x%x\n", loaded_config.size);
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen(loaded_config.hash) != 64) {
|
||||||
|
pr_err("Invalid saved config hash length: %zu\n", strlen(loaded_config.hash));
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hash format
|
||||||
|
for (i = 0; i < 64; i++) {
|
||||||
|
char c = loaded_config.hash[i];
|
||||||
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
|
||||||
|
pr_err("Invalid saved config hash character at position %d: %c\n", i, c);
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
dynamic_manager = loaded_config;
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
|
||||||
|
pr_info("Dynamic sign config loaded: size=0x%x, hash=%.16s...\n",
|
||||||
|
loaded_config.size, loaded_config.hash);
|
||||||
|
|
||||||
|
exit:
|
||||||
|
filp_close(fp, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool persistent_dynamic_manager(void)
|
||||||
|
{
|
||||||
|
return ksu_queue_work(&ksu_save_dynamic_manager_work);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void do_clear_dynamic_manager(struct work_struct *work)
|
||||||
|
{
|
||||||
|
loff_t off = 0;
|
||||||
|
struct file *fp;
|
||||||
|
char zero_buffer[512];
|
||||||
|
|
||||||
|
memset(zero_buffer, 0, sizeof(zero_buffer));
|
||||||
|
|
||||||
|
fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (IS_ERR(fp)) {
|
||||||
|
pr_err("clear_dynamic_manager create file failed: %ld\n", PTR_ERR(fp));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write null bytes to overwrite the file content
|
||||||
|
if (ksu_kernel_write_compat(fp, zero_buffer, sizeof(zero_buffer), &off) != sizeof(zero_buffer)) {
|
||||||
|
pr_err("clear_dynamic_manager write null bytes failed.\n");
|
||||||
|
} else {
|
||||||
|
pr_info("Dynamic sign config file cleared successfully\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
filp_close(fp, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool clear_dynamic_manager_file(void)
|
||||||
|
{
|
||||||
|
return ksu_queue_work(&ksu_clear_dynamic_manager_work);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ksu_handle_dynamic_manager(struct dynamic_manager_user_config *config)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
int ret = 0;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (config->operation) {
|
||||||
|
case DYNAMIC_MANAGER_OP_SET:
|
||||||
|
if (config->size < 0x100 || config->size > 0x1000) {
|
||||||
|
pr_err("invalid size: 0x%x\n", config->size);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strlen(config->hash) != 64) {
|
||||||
|
pr_err("invalid hash length: %zu\n", strlen(config->hash));
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hash format
|
||||||
|
for (i = 0; i < 64; i++) {
|
||||||
|
char c = config->hash[i];
|
||||||
|
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) {
|
||||||
|
pr_err("invalid hash character at position %d: %c\n", i, c);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
dynamic_manager.size = config->size;
|
||||||
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
|
||||||
|
strscpy(dynamic_manager.hash, config->hash, sizeof(dynamic_manager.hash));
|
||||||
|
#else
|
||||||
|
strlcpy(dynamic_manager.hash, config->hash, sizeof(dynamic_manager.hash));
|
||||||
|
#endif
|
||||||
|
dynamic_manager.is_set = 1;
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
|
||||||
|
persistent_dynamic_manager();
|
||||||
|
pr_info("dynamic manager updated: size=0x%x, hash=%.16s... (multi-manager enabled)\n",
|
||||||
|
config->size, config->hash);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DYNAMIC_MANAGER_OP_GET:
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
if (dynamic_manager.is_set) {
|
||||||
|
config->size = dynamic_manager.size;
|
||||||
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
|
||||||
|
strscpy(config->hash, dynamic_manager.hash, sizeof(config->hash));
|
||||||
|
#else
|
||||||
|
strlcpy(config->hash, dynamic_manager.hash, sizeof(config->hash));
|
||||||
|
#endif
|
||||||
|
ret = 0;
|
||||||
|
} else {
|
||||||
|
ret = -ENODATA;
|
||||||
|
}
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DYNAMIC_MANAGER_OP_CLEAR:
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
dynamic_manager.size = 0x300;
|
||||||
|
strcpy(dynamic_manager.hash, "0000000000000000000000000000000000000000000000000000000000000000");
|
||||||
|
dynamic_manager.is_set = 0;
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
|
||||||
|
// Clear only dynamic managers, preserve default manager
|
||||||
|
clear_dynamic_manager();
|
||||||
|
|
||||||
|
// Clear file using the same method as save
|
||||||
|
clear_dynamic_manager_file();
|
||||||
|
|
||||||
|
pr_info("Dynamic sign config cleared (multi-manager disabled)\n");
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
pr_err("Invalid dynamic manager operation: %d\n", config->operation);
|
||||||
|
return -EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ksu_load_dynamic_manager(void)
|
||||||
|
{
|
||||||
|
return ksu_queue_work(&ksu_load_dynamic_manager_work);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_dynamic_manager_init(void)
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
|
||||||
|
INIT_WORK(&ksu_save_dynamic_manager_work, do_save_dynamic_manager);
|
||||||
|
INIT_WORK(&ksu_load_dynamic_manager_work, do_load_dynamic_manager);
|
||||||
|
INIT_WORK(&ksu_clear_dynamic_manager_work, do_clear_dynamic_manager);
|
||||||
|
|
||||||
|
// Initialize manager slots
|
||||||
|
for (i = 0; i < MAX_MANAGERS; i++) {
|
||||||
|
active_managers[i].is_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ksu_load_dynamic_manager();
|
||||||
|
|
||||||
|
pr_info("Dynamic sign initialized with conditional multi-manager support\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_dynamic_manager_exit(void)
|
||||||
|
{
|
||||||
|
clear_dynamic_manager();
|
||||||
|
|
||||||
|
// Save current config before exit
|
||||||
|
do_save_dynamic_manager(NULL);
|
||||||
|
pr_info("Dynamic sign exited with persistent storage\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dynamic manager configuration for signature verification
|
||||||
|
bool ksu_get_dynamic_manager_config(unsigned int *size, const char **hash)
|
||||||
|
{
|
||||||
|
unsigned long flags;
|
||||||
|
bool valid = false;
|
||||||
|
|
||||||
|
spin_lock_irqsave(&dynamic_manager_lock, flags);
|
||||||
|
if (dynamic_manager.is_set) {
|
||||||
|
if (size) *size = dynamic_manager.size;
|
||||||
|
if (hash) *hash = dynamic_manager.hash;
|
||||||
|
valid = true;
|
||||||
|
}
|
||||||
|
spin_unlock_irqrestore(&dynamic_manager_lock, flags);
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
43
kernel/dynamic_manager.h
Normal file
43
kernel/dynamic_manager.h
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
#ifndef __KSU_H_DYNAMIC_MANAGER
|
||||||
|
#define __KSU_H_DYNAMIC_MANAGER
|
||||||
|
|
||||||
|
#include <linux/types.h>
|
||||||
|
#include "ksu.h"
|
||||||
|
|
||||||
|
#define DYNAMIC_MANAGER_FILE_MAGIC 0x7f445347 // 'DSG', u32
|
||||||
|
#define DYNAMIC_MANAGER_FILE_VERSION 1 // u32
|
||||||
|
#define KERNEL_SU_DYNAMIC_MANAGER "/data/adb/ksu/.dynamic_manager"
|
||||||
|
|
||||||
|
struct dynamic_manager_config {
|
||||||
|
unsigned int size;
|
||||||
|
char hash[65];
|
||||||
|
int is_set;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct manager_info {
|
||||||
|
uid_t uid;
|
||||||
|
int signature_index;
|
||||||
|
bool is_active;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dynamic sign operations
|
||||||
|
void ksu_dynamic_manager_init(void);
|
||||||
|
void ksu_dynamic_manager_exit(void);
|
||||||
|
int ksu_handle_dynamic_manager(struct dynamic_manager_user_config *config);
|
||||||
|
bool ksu_load_dynamic_manager(void);
|
||||||
|
bool ksu_is_dynamic_manager_enabled(void);
|
||||||
|
|
||||||
|
// Multi-manager operations
|
||||||
|
void ksu_add_manager(uid_t uid, int signature_index);
|
||||||
|
void ksu_remove_manager(uid_t uid);
|
||||||
|
bool ksu_is_any_manager(uid_t uid);
|
||||||
|
int ksu_get_manager_signature_index(uid_t uid);
|
||||||
|
int ksu_get_active_managers(struct manager_list_info *info);
|
||||||
|
|
||||||
|
// Multi-manager APK verification
|
||||||
|
bool ksu_is_dynamic_manager_apk(char *path, int *signature_index);
|
||||||
|
|
||||||
|
// Configuration access for signature verification
|
||||||
|
bool ksu_get_dynamic_manager_config(unsigned int *size, const char **hash);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -60,6 +60,13 @@ uid_t sukisu_get_manager_uid() {
|
|||||||
return ksu_manager_uid;
|
return ksu_manager_uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static
|
||||||
|
void sukisu_set_manager_uid(uid_t uid, int force) {
|
||||||
|
if(force || ksu_manager_uid == -1) {
|
||||||
|
ksu_manager_uid = uid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======================================================================
|
// ======================================================================
|
||||||
|
|
||||||
struct CompactAddressSymbol {
|
struct CompactAddressSymbol {
|
||||||
@@ -75,7 +82,8 @@ static struct CompactAddressSymbol address_symbol [] = {
|
|||||||
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
|
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
|
||||||
{ "is_uid_should_umount", &sukisu_is_uid_should_umount },
|
{ "is_uid_should_umount", &sukisu_is_uid_should_umount },
|
||||||
{ "is_current_uid_manager", &sukisu_is_current_uid_manager },
|
{ "is_current_uid_manager", &sukisu_is_current_uid_manager },
|
||||||
{ "get_manager_uid", &sukisu_get_manager_uid }
|
{ "get_manager_uid", &sukisu_get_manager_uid },
|
||||||
|
{ "sukisu_set_manager_uid", &sukisu_set_manager_uid }
|
||||||
};
|
};
|
||||||
|
|
||||||
unsigned long sukisu_compact_find_symbol(const char* name) {
|
unsigned long sukisu_compact_find_symbol(const char* name) {
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ void sukisu_kpm_load_module_path(const char* path, const char* args, void* ptr,
|
|||||||
int res = -1;
|
int res = -1;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_load_module_path). path=%s args=%s ptr=%p\n", path, args, ptr);
|
printk("KPM: Stub function called (sukisu_kpm_load_module_path). path=%s args=%s ptr=%p\n", path, args, ptr);
|
||||||
__asm__ volatile("nop"); // 精确控制循环不被优化
|
__asm__ volatile("nop"); // 精确控制循环不被优化
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
noinline
|
noinline
|
||||||
@@ -73,7 +73,7 @@ void sukisu_kpm_unload_module(const char* name, void* ptr, void __user* result)
|
|||||||
int res = -1;
|
int res = -1;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_unload_module). name=%s ptr=%p\n", name, ptr);
|
printk("KPM: Stub function called (sukisu_kpm_unload_module). name=%s ptr=%p\n", name, ptr);
|
||||||
__asm__ volatile("nop"); // 精确控制循环不被优化
|
__asm__ volatile("nop"); // 精确控制循环不被优化
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
noinline
|
noinline
|
||||||
@@ -83,7 +83,7 @@ void sukisu_kpm_num(void __user* result) {
|
|||||||
int res = 0;
|
int res = 0;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_num).\n");
|
printk("KPM: Stub function called (sukisu_kpm_num).\n");
|
||||||
__asm__ volatile("nop"); // 精确控制循环不被优化
|
__asm__ volatile("nop"); // 精确控制循环不被优化
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
noinline
|
noinline
|
||||||
@@ -93,7 +93,7 @@ void sukisu_kpm_info(const char* name, void __user* out, void __user* result) {
|
|||||||
int res = -1;
|
int res = -1;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_info). name=%s buffer=%p\n", name, out);
|
printk("KPM: Stub function called (sukisu_kpm_info). name=%s buffer=%p\n", name, out);
|
||||||
__asm__ volatile("nop"); // 精确控制循环不被优化
|
__asm__ volatile("nop"); // 精确控制循环不被优化
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
noinline
|
noinline
|
||||||
@@ -102,7 +102,7 @@ void sukisu_kpm_list(void __user* out, unsigned int bufferSize, void __user* res
|
|||||||
// This is a KPM module stub.
|
// This is a KPM module stub.
|
||||||
int res = -1;
|
int res = -1;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_list). buffer=%p size=%d\n", out, bufferSize);
|
printk("KPM: Stub function called (sukisu_kpm_list). buffer=%p size=%d\n", out, bufferSize);
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
noinline
|
noinline
|
||||||
@@ -112,7 +112,7 @@ void sukisu_kpm_control(void __user* name, void __user* args, void __user* resul
|
|||||||
int res = -1;
|
int res = -1;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_control). name=%p args=%p\n", name, args);
|
printk("KPM: Stub function called (sukisu_kpm_control). name=%p args=%p\n", name, args);
|
||||||
__asm__ volatile("nop"); // 精确控制循环不被优化
|
__asm__ volatile("nop"); // 精确控制循环不被优化
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
noinline
|
noinline
|
||||||
@@ -120,7 +120,7 @@ NO_OPTIMIZE
|
|||||||
void sukisu_kpm_version(void __user* out, unsigned int bufferSize, void __user* result) {
|
void sukisu_kpm_version(void __user* out, unsigned int bufferSize, void __user* result) {
|
||||||
int res = -1;
|
int res = -1;
|
||||||
printk("KPM: Stub function called (sukisu_kpm_version). buffer=%p size=%d\n", out, bufferSize);
|
printk("KPM: Stub function called (sukisu_kpm_version). buffer=%p size=%d\n", out, bufferSize);
|
||||||
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user faild.");
|
if(copy_to_user(result, &res, sizeof(res)) < 1) printk("KPM: Copy to user failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
EXPORT_SYMBOL(sukisu_kpm_load_module_path);
|
EXPORT_SYMBOL(sukisu_kpm_load_module_path);
|
||||||
|
|||||||
@@ -167,7 +167,11 @@ DYNAMIC_STRUCT_BEGIN(task_struct)
|
|||||||
DEFINE_MEMBER(task_struct, group_leader)
|
DEFINE_MEMBER(task_struct, group_leader)
|
||||||
DEFINE_MEMBER(task_struct, mm)
|
DEFINE_MEMBER(task_struct, mm)
|
||||||
DEFINE_MEMBER(task_struct, active_mm)
|
DEFINE_MEMBER(task_struct, active_mm)
|
||||||
|
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
|
||||||
|
DEFINE_MEMBER(task_struct, pids[PIDTYPE_PID].pid)
|
||||||
|
#else
|
||||||
DEFINE_MEMBER(task_struct, thread_pid)
|
DEFINE_MEMBER(task_struct, thread_pid)
|
||||||
|
#endif
|
||||||
DEFINE_MEMBER(task_struct, files)
|
DEFINE_MEMBER(task_struct, files)
|
||||||
DEFINE_MEMBER(task_struct, seccomp)
|
DEFINE_MEMBER(task_struct, seccomp)
|
||||||
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
#ifdef CONFIG_THREAD_INFO_IN_TASK
|
||||||
|
|||||||
16
kernel/ksu.c
16
kernel/ksu.c
@@ -36,6 +36,10 @@ extern void ksu_sucompat_init();
|
|||||||
extern void ksu_sucompat_exit();
|
extern void ksu_sucompat_exit();
|
||||||
extern void ksu_ksud_init();
|
extern void ksu_ksud_init();
|
||||||
extern void ksu_ksud_exit();
|
extern void ksu_ksud_exit();
|
||||||
|
#ifdef CONFIG_KSU_TRACEPOINT_HOOK
|
||||||
|
extern void ksu_trace_register();
|
||||||
|
extern void ksu_trace_unregister();
|
||||||
|
#endif
|
||||||
|
|
||||||
int __init kernelsu_init(void)
|
int __init kernelsu_init(void)
|
||||||
{
|
{
|
||||||
@@ -56,13 +60,17 @@ int __init kernelsu_init(void)
|
|||||||
ksu_allowlist_init();
|
ksu_allowlist_init();
|
||||||
|
|
||||||
ksu_throne_tracker_init();
|
ksu_throne_tracker_init();
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
ksu_sucompat_init();
|
ksu_sucompat_init();
|
||||||
ksu_ksud_init();
|
ksu_ksud_init();
|
||||||
#else
|
#else
|
||||||
pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html");
|
pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef CONFIG_KSU_TRACEPOINT_HOOK
|
||||||
|
ksu_trace_register();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef MODULE
|
#ifdef MODULE
|
||||||
#ifndef CONFIG_KSU_DEBUG
|
#ifndef CONFIG_KSU_DEBUG
|
||||||
kobject_del(&THIS_MODULE->mkobj.kobj);
|
kobject_del(&THIS_MODULE->mkobj.kobj);
|
||||||
@@ -79,11 +87,15 @@ void kernelsu_exit(void)
|
|||||||
|
|
||||||
destroy_workqueue(ksu_workqueue);
|
destroy_workqueue(ksu_workqueue);
|
||||||
|
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
ksu_ksud_exit();
|
ksu_ksud_exit();
|
||||||
ksu_sucompat_exit();
|
ksu_sucompat_exit();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef CONFIG_KSU_TRACEPOINT_HOOK
|
||||||
|
ksu_trace_unregister();
|
||||||
|
#endif
|
||||||
|
|
||||||
ksu_core_exit();
|
ksu_core_exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
31
kernel/ksu.h
31
kernel/ksu.h
@@ -24,6 +24,13 @@
|
|||||||
#define CMD_IS_SU_ENABLED 14
|
#define CMD_IS_SU_ENABLED 14
|
||||||
#define CMD_ENABLE_SU 15
|
#define CMD_ENABLE_SU 15
|
||||||
|
|
||||||
|
#define CMD_GET_FULL_VERSION 0xC0FFEE1A
|
||||||
|
|
||||||
|
#define CMD_ENABLE_KPM 100
|
||||||
|
#define CMD_HOOK_TYPE 101
|
||||||
|
#define CMD_DYNAMIC_MANAGER 103
|
||||||
|
#define CMD_GET_MANAGERS 104
|
||||||
|
|
||||||
#define EVENT_POST_FS_DATA 1
|
#define EVENT_POST_FS_DATA 1
|
||||||
#define EVENT_BOOT_COMPLETED 2
|
#define EVENT_BOOT_COMPLETED 2
|
||||||
#define EVENT_MODULE_MOUNTED 3
|
#define EVENT_MODULE_MOUNTED 3
|
||||||
@@ -34,6 +41,30 @@
|
|||||||
#define KSU_MAX_GROUPS 32
|
#define KSU_MAX_GROUPS 32
|
||||||
#define KSU_SELINUX_DOMAIN 64
|
#define KSU_SELINUX_DOMAIN 64
|
||||||
|
|
||||||
|
// SukiSU Ultra kernel su version full strings
|
||||||
|
#ifndef KSU_VERSION_FULL
|
||||||
|
#define KSU_VERSION_FULL "v3.x-00000000@unknown"
|
||||||
|
#endif
|
||||||
|
#define KSU_FULL_VERSION_STRING 255
|
||||||
|
|
||||||
|
#define DYNAMIC_MANAGER_OP_SET 0
|
||||||
|
#define DYNAMIC_MANAGER_OP_GET 1
|
||||||
|
#define DYNAMIC_MANAGER_OP_CLEAR 2
|
||||||
|
|
||||||
|
struct dynamic_manager_user_config {
|
||||||
|
unsigned int operation;
|
||||||
|
unsigned int size;
|
||||||
|
char hash[65];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct manager_list_info {
|
||||||
|
int count;
|
||||||
|
struct {
|
||||||
|
uid_t uid;
|
||||||
|
int signature_index;
|
||||||
|
} managers[2];
|
||||||
|
};
|
||||||
|
|
||||||
struct root_profile {
|
struct root_profile {
|
||||||
int32_t uid;
|
int32_t uid;
|
||||||
int32_t gid;
|
int32_t gid;
|
||||||
|
|||||||
90
kernel/ksu_trace.c
Normal file
90
kernel/ksu_trace.c
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
#include "ksu_trace.h"
|
||||||
|
|
||||||
|
|
||||||
|
// extern kernelsu functions
|
||||||
|
extern bool ksu_execveat_hook __read_mostly;
|
||||||
|
extern bool ksu_vfs_read_hook __read_mostly;
|
||||||
|
extern bool ksu_input_hook __read_mostly;
|
||||||
|
extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags);
|
||||||
|
extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags);
|
||||||
|
extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, int *flags);
|
||||||
|
extern int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr, size_t *count_ptr);
|
||||||
|
extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);
|
||||||
|
extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);
|
||||||
|
extern int ksu_handle_devpts(struct inode*);
|
||||||
|
// end kernelsu functions
|
||||||
|
|
||||||
|
|
||||||
|
// tracepoint callback functions
|
||||||
|
void ksu_trace_execveat_hook_callback(void *data, int *fd, struct filename **filename_ptr,
|
||||||
|
void *argv, void *envp, int *flags)
|
||||||
|
{
|
||||||
|
if (unlikely(ksu_execveat_hook))
|
||||||
|
ksu_handle_execveat(fd, filename_ptr, argv, envp, flags);
|
||||||
|
else
|
||||||
|
ksu_handle_execveat_sucompat(fd, filename_ptr, NULL, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_trace_execveat_sucompat_hook_callback(void *data, int *fd, struct filename **filename_ptr,
|
||||||
|
void *argv, void *envp, int *flags)
|
||||||
|
{
|
||||||
|
if (!ksu_execveat_hook)
|
||||||
|
ksu_handle_execveat_sucompat(fd, filename_ptr, argv, envp, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_trace_faccessat_hook_callback(void *data, int *dfd, const char __user **filename_user,
|
||||||
|
int *mode, int *flags)
|
||||||
|
{
|
||||||
|
ksu_handle_faccessat(dfd, filename_user, mode, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_trace_sys_read_hook_callback(void *data, unsigned int fd, char __user **buf_ptr,
|
||||||
|
size_t *count_ptr)
|
||||||
|
{
|
||||||
|
if (unlikely(ksu_vfs_read_hook))
|
||||||
|
ksu_handle_sys_read(fd, buf_ptr, count_ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_trace_stat_hook_callback(void *data, int *dfd, const char __user **filename_user,
|
||||||
|
int *flags)
|
||||||
|
{
|
||||||
|
ksu_handle_stat(dfd, filename_user, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_trace_input_hook_callback(void *data, unsigned int *type, unsigned int *code,
|
||||||
|
int *value)
|
||||||
|
{
|
||||||
|
if (unlikely(ksu_input_hook))
|
||||||
|
ksu_handle_input_handle_event(type, code, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ksu_trace_devpts_hook_callback(void *data, struct inode *inode)
|
||||||
|
{
|
||||||
|
ksu_handle_devpts(inode);
|
||||||
|
}
|
||||||
|
// end tracepoint callback functions
|
||||||
|
|
||||||
|
|
||||||
|
// register tracepoint callback functions
|
||||||
|
void ksu_trace_register(void)
|
||||||
|
{
|
||||||
|
register_trace_ksu_trace_execveat_hook(ksu_trace_execveat_hook_callback, NULL);
|
||||||
|
register_trace_ksu_trace_execveat_sucompat_hook(ksu_trace_execveat_sucompat_hook_callback, NULL);
|
||||||
|
register_trace_ksu_trace_faccessat_hook(ksu_trace_faccessat_hook_callback, NULL);
|
||||||
|
register_trace_ksu_trace_sys_read_hook(ksu_trace_sys_read_hook_callback, NULL);
|
||||||
|
register_trace_ksu_trace_stat_hook(ksu_trace_stat_hook_callback, NULL);
|
||||||
|
register_trace_ksu_trace_input_hook(ksu_trace_input_hook_callback, NULL);
|
||||||
|
register_trace_ksu_trace_devpts_hook(ksu_trace_devpts_hook_callback, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unregister tracepoint callback functions
|
||||||
|
void ksu_trace_unregister(void)
|
||||||
|
{
|
||||||
|
unregister_trace_ksu_trace_execveat_hook(ksu_trace_execveat_hook_callback, NULL);
|
||||||
|
unregister_trace_ksu_trace_execveat_sucompat_hook(ksu_trace_execveat_sucompat_hook_callback, NULL);
|
||||||
|
unregister_trace_ksu_trace_faccessat_hook(ksu_trace_faccessat_hook_callback, NULL);
|
||||||
|
unregister_trace_ksu_trace_sys_read_hook(ksu_trace_sys_read_hook_callback, NULL);
|
||||||
|
unregister_trace_ksu_trace_stat_hook(ksu_trace_stat_hook_callback, NULL);
|
||||||
|
unregister_trace_ksu_trace_input_hook(ksu_trace_input_hook_callback, NULL);
|
||||||
|
unregister_trace_ksu_trace_devpts_hook(ksu_trace_devpts_hook_callback, NULL);
|
||||||
|
}
|
||||||
45
kernel/ksu_trace.h
Normal file
45
kernel/ksu_trace.h
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#undef TRACE_SYSTEM
|
||||||
|
#define TRACE_SYSTEM ksu_trace
|
||||||
|
|
||||||
|
#if !defined(_KSU_TRACE_H) || defined(TRACE_HEADER_MULTI_READ)
|
||||||
|
#define _KSU_TRACE_H
|
||||||
|
|
||||||
|
#include <linux/fs.h>
|
||||||
|
#include <linux/tracepoint.h>
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_execveat_hook,
|
||||||
|
TP_PROTO(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags),
|
||||||
|
TP_ARGS(fd, filename_ptr, argv, envp, flags));
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_execveat_sucompat_hook,
|
||||||
|
TP_PROTO(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags),
|
||||||
|
TP_ARGS(fd, filename_ptr, argv, envp, flags));
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_faccessat_hook,
|
||||||
|
TP_PROTO(int *dfd, const char __user **filename_user, int *mode, int *flags),
|
||||||
|
TP_ARGS(dfd, filename_user, mode, flags));
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_sys_read_hook,
|
||||||
|
TP_PROTO(unsigned int fd, char __user **buf_ptr, size_t *count_ptr),
|
||||||
|
TP_ARGS(fd, buf_ptr, count_ptr));
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_stat_hook,
|
||||||
|
TP_PROTO(int *dfd, const char __user **filename_user, int *flags),
|
||||||
|
TP_ARGS(dfd, filename_user, flags));
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_input_hook,
|
||||||
|
TP_PROTO(unsigned int *type, unsigned int *code, int *value),
|
||||||
|
TP_ARGS(type, code, value));
|
||||||
|
|
||||||
|
DECLARE_TRACE(ksu_trace_devpts_hook,
|
||||||
|
TP_PROTO(struct inode *inode),
|
||||||
|
TP_ARGS(inode));
|
||||||
|
|
||||||
|
#endif /* _KSU_TRACE_H */
|
||||||
|
|
||||||
|
#undef TRACE_INCLUDE_PATH
|
||||||
|
#define TRACE_INCLUDE_PATH .
|
||||||
|
#undef TRACE_INCLUDE_FILE
|
||||||
|
#define TRACE_INCLUDE_FILE ksu_trace
|
||||||
|
|
||||||
|
#include <trace/define_trace.h>
|
||||||
10
kernel/ksu_trace_export.c
Normal file
10
kernel/ksu_trace_export.c
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#define CREATE_TRACE_POINTS
|
||||||
|
#include "ksu_trace.h"
|
||||||
|
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_execveat_hook);
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_execveat_sucompat_hook);
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_faccessat_hook);
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_sys_read_hook);
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_stat_hook);
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_input_hook);
|
||||||
|
EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_devpts_hook);
|
||||||
@@ -48,7 +48,7 @@ static void stop_vfs_read_hook();
|
|||||||
static void stop_execve_hook();
|
static void stop_execve_hook();
|
||||||
static void stop_input_hook();
|
static void stop_input_hook();
|
||||||
|
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
static struct work_struct stop_vfs_read_work;
|
static struct work_struct stop_vfs_read_work;
|
||||||
static struct work_struct stop_execve_hook_work;
|
static struct work_struct stop_execve_hook_work;
|
||||||
static struct work_struct stop_input_hook_work;
|
static struct work_struct stop_input_hook_work;
|
||||||
@@ -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
|
||||||
@@ -157,7 +162,7 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,
|
|||||||
struct user_arg_ptr *argv,
|
struct user_arg_ptr *argv,
|
||||||
struct user_arg_ptr *envp, int *flags)
|
struct user_arg_ptr *envp, int *flags)
|
||||||
{
|
{
|
||||||
#ifndef CONFIG_KPROBES
|
#ifndef CONFIG_KSU_KPROBES_HOOK
|
||||||
if (!ksu_execveat_hook) {
|
if (!ksu_execveat_hook) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -313,7 +318,7 @@ static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to)
|
|||||||
int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,
|
int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,
|
||||||
size_t *count_ptr, loff_t **pos)
|
size_t *count_ptr, loff_t **pos)
|
||||||
{
|
{
|
||||||
#ifndef CONFIG_KPROBES
|
#ifndef CONFIG_KSU_KPROBES_HOOK
|
||||||
if (!ksu_vfs_read_hook) {
|
if (!ksu_vfs_read_hook) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -426,7 +431,7 @@ static bool is_volumedown_enough(unsigned int count)
|
|||||||
int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,
|
int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,
|
||||||
int *value)
|
int *value)
|
||||||
{
|
{
|
||||||
#ifndef CONFIG_KPROBES
|
#ifndef CONFIG_KSU_KPROBES_HOOK
|
||||||
if (!ksu_input_hook) {
|
if (!ksu_input_hook) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -468,7 +473,7 @@ bool ksu_is_safe_mode()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||||
{
|
{
|
||||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||||
@@ -544,7 +549,7 @@ static void do_stop_input_hook(struct work_struct *work)
|
|||||||
|
|
||||||
static void stop_vfs_read_hook()
|
static void stop_vfs_read_hook()
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
bool ret = schedule_work(&stop_vfs_read_work);
|
bool ret = schedule_work(&stop_vfs_read_work);
|
||||||
pr_info("unregister vfs_read kprobe: %d!\n", ret);
|
pr_info("unregister vfs_read kprobe: %d!\n", ret);
|
||||||
#else
|
#else
|
||||||
@@ -555,7 +560,7 @@ static void stop_vfs_read_hook()
|
|||||||
|
|
||||||
static void stop_execve_hook()
|
static void stop_execve_hook()
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
bool ret = schedule_work(&stop_execve_hook_work);
|
bool ret = schedule_work(&stop_execve_hook_work);
|
||||||
pr_info("unregister execve kprobe: %d!\n", ret);
|
pr_info("unregister execve kprobe: %d!\n", ret);
|
||||||
#else
|
#else
|
||||||
@@ -571,7 +576,7 @@ static void stop_input_hook()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
input_hook_stopped = true;
|
input_hook_stopped = true;
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
bool ret = schedule_work(&stop_input_hook_work);
|
bool ret = schedule_work(&stop_input_hook_work);
|
||||||
pr_info("unregister input kprobe: %d!\n", ret);
|
pr_info("unregister input kprobe: %d!\n", ret);
|
||||||
#else
|
#else
|
||||||
@@ -583,7 +588,7 @@ static void stop_input_hook()
|
|||||||
// ksud: module support
|
// ksud: module support
|
||||||
void ksu_ksud_init()
|
void ksu_ksud_init()
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
ret = register_kprobe(&execve_kp);
|
ret = register_kprobe(&execve_kp);
|
||||||
@@ -603,12 +608,12 @@ void ksu_ksud_init()
|
|||||||
|
|
||||||
void ksu_ksud_exit()
|
void ksu_ksud_exit()
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
unregister_kprobe(&execve_kp);
|
unregister_kprobe(&execve_kp);
|
||||||
// this should be done before unregister vfs_read_kp
|
// this should be done before unregister vfs_read_kp
|
||||||
// unregister_kprobe(&vfs_read_kp);
|
// unregister_kprobe(&vfs_read_kp);
|
||||||
unregister_kprobe(&input_event_kp);
|
unregister_kprobe(&input_event_kp);
|
||||||
|
#endif
|
||||||
|
|
||||||
is_boot_phase = false;
|
is_boot_phase = false;
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,11 @@
|
|||||||
|
|
||||||
extern uid_t ksu_manager_uid; // DO NOT DIRECT USE
|
extern uid_t ksu_manager_uid; // DO NOT DIRECT USE
|
||||||
|
|
||||||
|
extern bool ksu_is_any_manager(uid_t uid);
|
||||||
|
extern void ksu_add_manager(uid_t uid, int signature_index);
|
||||||
|
extern void ksu_remove_manager(uid_t uid);
|
||||||
|
extern int ksu_get_manager_signature_index(uid_t uid);
|
||||||
|
|
||||||
static inline bool ksu_is_manager_uid_valid()
|
static inline bool ksu_is_manager_uid_valid()
|
||||||
{
|
{
|
||||||
return ksu_manager_uid != KSU_INVALID_UID;
|
return ksu_manager_uid != KSU_INVALID_UID;
|
||||||
@@ -15,7 +20,7 @@ static inline bool ksu_is_manager_uid_valid()
|
|||||||
|
|
||||||
static inline bool is_manager()
|
static inline bool is_manager()
|
||||||
{
|
{
|
||||||
return unlikely(ksu_manager_uid == current_uid().val);
|
return unlikely(ksu_is_any_manager(current_uid().val) || ksu_manager_uid == current_uid().val);
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline uid_t ksu_get_manager_uid()
|
static inline uid_t ksu_get_manager_uid()
|
||||||
|
|||||||
13
kernel/manager_sign.h
Normal file
13
kernel/manager_sign.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef MANAGER_SIGN_H
|
||||||
|
#define MANAGER_SIGN_H
|
||||||
|
|
||||||
|
// ShirkNeko/SukiSU
|
||||||
|
#define EXPECTED_SIZE_SHIRKNEKO 0x35c
|
||||||
|
#define EXPECTED_HASH_SHIRKNEKO "947ae944f3de4ed4c21a7e4f7953ecf351bfa2b36239da37a34111ad29993eef"
|
||||||
|
|
||||||
|
// Dynamic Sign
|
||||||
|
#define EXPECTED_SIZE_OTHER 0x300
|
||||||
|
#define EXPECTED_HASH_OTHER "0000000000000000000000000000000000000000000000000000000000000000"
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* MANAGER_SIGN_H */
|
||||||
@@ -19,19 +19,24 @@
|
|||||||
static struct policydb *get_policydb(void)
|
static struct policydb *get_policydb(void)
|
||||||
{
|
{
|
||||||
struct policydb *db;
|
struct policydb *db;
|
||||||
struct selinux_policy *policy = rcu_dereference(selinux_state.policy);
|
struct selinux_policy *policy = selinux_state.policy;
|
||||||
db = &policy->policydb;
|
db = &policy->policydb;
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static DEFINE_MUTEX(ksu_rules);
|
||||||
|
|
||||||
void apply_kernelsu_rules()
|
void apply_kernelsu_rules()
|
||||||
{
|
{
|
||||||
|
struct policydb *db;
|
||||||
|
|
||||||
if (!getenforce()) {
|
if (!getenforce()) {
|
||||||
pr_info("SELinux permissive or disabled, apply rules!\n");
|
pr_info("SELinux permissive or disabled, apply rules!\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
rcu_read_lock();
|
mutex_lock(&ksu_rules);
|
||||||
struct policydb *db = get_policydb();
|
|
||||||
|
db = get_policydb();
|
||||||
|
|
||||||
ksu_permissive(db, KERNEL_SU_DOMAIN);
|
ksu_permissive(db, KERNEL_SU_DOMAIN);
|
||||||
ksu_typeattribute(db, KERNEL_SU_DOMAIN, "mlstrustedsubject");
|
ksu_typeattribute(db, KERNEL_SU_DOMAIN, "mlstrustedsubject");
|
||||||
@@ -122,7 +127,10 @@ void apply_kernelsu_rules()
|
|||||||
ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "getpgid");
|
ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "getpgid");
|
||||||
ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "sigkill");
|
ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "sigkill");
|
||||||
|
|
||||||
rcu_read_unlock();
|
// https://android-review.googlesource.com/c/platform/system/logging/+/3725346
|
||||||
|
ksu_dontaudit(db, "untrusted_app", KERNEL_SU_DOMAIN, "dir", "getattr");
|
||||||
|
|
||||||
|
mutex_unlock(&ksu_rules);
|
||||||
}
|
}
|
||||||
|
|
||||||
#define MAX_SEPOL_LEN 128
|
#define MAX_SEPOL_LEN 128
|
||||||
@@ -137,17 +145,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)
|
||||||
@@ -184,6 +220,8 @@ static void reset_avc_cache()
|
|||||||
|
|
||||||
int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
||||||
{
|
{
|
||||||
|
struct policydb *db;
|
||||||
|
|
||||||
if (!arg4) {
|
if (!arg4) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -192,18 +230,62 @@ 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;
|
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;
|
||||||
|
sepol2 = data.field_sepol2;
|
||||||
|
sepol3 = data.field_sepol3;
|
||||||
|
sepol4 = data.field_sepol4;
|
||||||
|
sepol5 = data.field_sepol5;
|
||||||
|
sepol6 = data.field_sepol6;
|
||||||
|
sepol7 = data.field_sepol7;
|
||||||
|
cmd = data.cmd;
|
||||||
|
subcmd = data.subcmd;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
// basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n)
|
||||||
|
struct sepol_data data;
|
||||||
|
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
|
||||||
|
pr_err("sepol: copy sepol_data failed.\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sepol1 = data.field_sepol1;
|
||||||
|
sepol2 = data.field_sepol2;
|
||||||
|
sepol3 = data.field_sepol3;
|
||||||
|
sepol4 = data.field_sepol4;
|
||||||
|
sepol5 = data.field_sepol5;
|
||||||
|
sepol6 = data.field_sepol6;
|
||||||
|
sepol7 = data.field_sepol7;
|
||||||
|
cmd = data.cmd;
|
||||||
|
subcmd = data.subcmd;
|
||||||
|
#endif
|
||||||
|
|
||||||
u32 cmd = data.cmd;
|
mutex_lock(&ksu_rules);
|
||||||
u32 subcmd = data.subcmd;
|
|
||||||
|
|
||||||
rcu_read_lock();
|
db = get_policydb();
|
||||||
|
|
||||||
struct policydb *db = get_policydb();
|
|
||||||
|
|
||||||
int ret = -1;
|
int ret = -1;
|
||||||
if (cmd == CMD_NORMAL_PERM) {
|
if (cmd == CMD_NORMAL_PERM) {
|
||||||
@@ -213,22 +295,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 +340,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 +377,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 +397,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 +421,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 +438,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 +478,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 +511,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;
|
||||||
@@ -453,7 +535,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
|
|||||||
}
|
}
|
||||||
|
|
||||||
exit:
|
exit:
|
||||||
rcu_read_unlock();
|
mutex_unlock(&ksu_rules);
|
||||||
|
|
||||||
// only allow and xallow needs to reset avc cache, but we cannot do that because
|
// only allow and xallow needs to reset avc cache, but we cannot do that because
|
||||||
// we are in atomic context. so we just reset it every time.
|
// we are in atomic context. so we just reset it every time.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
GKI_ROOT=$(pwd)
|
KERNEL_ROOT=$(pwd)
|
||||||
|
|
||||||
display_usage() {
|
display_usage() {
|
||||||
echo "Usage: $0 [--cleanup | <commit-or-tag>]"
|
echo "Usage: $0 [--cleanup | <commit-or-tag>]"
|
||||||
@@ -12,10 +12,10 @@ display_usage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialize_variables() {
|
initialize_variables() {
|
||||||
if test -d "$GKI_ROOT/common/drivers"; then
|
if test -d "$KERNEL_ROOT/common/drivers"; then
|
||||||
DRIVER_DIR="$GKI_ROOT/common/drivers"
|
DRIVER_DIR="$KERNEL_ROOT/common/drivers"
|
||||||
elif test -d "$GKI_ROOT/drivers"; then
|
elif test -d "$KERNEL_ROOT/drivers"; then
|
||||||
DRIVER_DIR="$GKI_ROOT/drivers"
|
DRIVER_DIR="$KERNEL_ROOT/drivers"
|
||||||
else
|
else
|
||||||
echo '[ERROR] "drivers/" directory not found.'
|
echo '[ERROR] "drivers/" directory not found.'
|
||||||
exit 127
|
exit 127
|
||||||
@@ -30,22 +30,21 @@ perform_cleanup() {
|
|||||||
echo "[+] Cleaning up..."
|
echo "[+] Cleaning up..."
|
||||||
[ -L "$DRIVER_DIR/kernelsu" ] && rm "$DRIVER_DIR/kernelsu" && echo "[-] Symlink removed."
|
[ -L "$DRIVER_DIR/kernelsu" ] && rm "$DRIVER_DIR/kernelsu" && echo "[-] Symlink removed."
|
||||||
grep -q "kernelsu" "$DRIVER_MAKEFILE" && sed -i '/kernelsu/d' "$DRIVER_MAKEFILE" && echo "[-] Makefile reverted."
|
grep -q "kernelsu" "$DRIVER_MAKEFILE" && sed -i '/kernelsu/d' "$DRIVER_MAKEFILE" && echo "[-] Makefile reverted."
|
||||||
grep -q "drivers/kernelsu/Kconfig" "$DRIVER_KCONFIG" && sed -i '/drivers\/kernelsu\/Kconfig/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted."
|
grep -q "kernelsu" "$DRIVER_KCONFIG" && sed -i '/kernelsu/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted."
|
||||||
if [ -d "$GKI_ROOT/KernelSU" ]; then
|
if [ -d "$KERNEL_ROOT/KernelSU" ]; then
|
||||||
rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted."
|
rm -rf "$KERNEL_ROOT/KernelSU" && echo "[-] KernelSU directory deleted."
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Sets up or update KernelSU environment
|
# Sets up or update KernelSU environment
|
||||||
setup_kernelsu() {
|
setup_kernelsu() {
|
||||||
echo "[+] Setting up KernelSU..."
|
echo "[+] Setting up KernelSU..."
|
||||||
# Clone the repository and rename it to KernelSU
|
# Clone the repository
|
||||||
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
|
if [ ! -d "$KERNEL_ROOT/KernelSU" ]; then
|
||||||
git clone https://github.com/ShirkNeko/SukiSU-Ultra SukiSU-Ultra
|
git clone https://github.com/SukiSU-Ultra/SukiSU-Ultra KernelSU
|
||||||
mv SukiSU-Ultra KernelSU
|
echo "[+] Repository cloned."
|
||||||
echo "[+] Repository cloned and renamed to KernelSU."
|
|
||||||
fi
|
fi
|
||||||
cd "$GKI_ROOT/KernelSU"
|
cd "$KERNEL_ROOT/KernelSU"
|
||||||
git stash && echo "[-] Stashed current changes."
|
git stash && echo "[-] Stashed current changes."
|
||||||
if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then
|
if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then
|
||||||
git checkout main && echo "[-] Switched to main branch."
|
git checkout main && echo "[-] Switched to main branch."
|
||||||
@@ -57,11 +56,11 @@ setup_kernelsu() {
|
|||||||
git checkout "$1" && echo "[-] Checked out $1." || echo "[-] Checkout default branch"
|
git checkout "$1" && echo "[-] Checked out $1." || echo "[-] Checkout default branch"
|
||||||
fi
|
fi
|
||||||
cd "$DRIVER_DIR"
|
cd "$DRIVER_DIR"
|
||||||
ln -sf "$(realpath --relative-to="$DRIVER_DIR" "$GKI_ROOT/KernelSU/kernel")" "kernelsu" && echo "[+] Symlink created."
|
ln -sf "$(realpath --relative-to="$DRIVER_DIR" "$KERNEL_ROOT/KernelSU/kernel")" "kernelsu" && echo "[+] Symlink created."
|
||||||
|
|
||||||
# Add entries in Makefile and Kconfig if not already existing
|
# Add entries in Makefile and Kconfig if not already existing
|
||||||
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile."
|
grep -q "kernelsu" "$DRIVER_MAKEFILE" || echo 'obj-$(CONFIG_KSU) += kernelsu/' >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile."
|
||||||
grep -q "source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig."
|
grep -q 'source "drivers/kernelsu/Kconfig"' "$DRIVER_KCONFIG" || sed -i '/endmenu/i\source "drivers/kernelsu/Kconfig"' "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig."
|
||||||
echo '[+] Done.'
|
echo '[+] Done.'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,10 @@
|
|||||||
|
|
||||||
extern void escape_to_root();
|
extern void escape_to_root();
|
||||||
|
|
||||||
|
#ifndef CONFIG_KSU_KPROBES_HOOK
|
||||||
|
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_KSU_KPROBES_HOOK
|
||||||
|
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_KSU_KPROBES_HOOK
|
||||||
|
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_KSU_KPROBES_HOOK
|
||||||
|
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_KSU_KPROBES_HOOK
|
||||||
|
if (!ksu_sucompat_non_kp){
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (unlikely(!filename_user))
|
if (unlikely(!filename_user))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
@@ -164,7 +189,48 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CONFIG_KPROBES
|
static int ksu_inline_handle_devpts(struct inode *inode)
|
||||||
|
{
|
||||||
|
if (!current->mm) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uid_t uid = current_uid().val;
|
||||||
|
if (uid % 100000 < 10000) {
|
||||||
|
// not untrusted_app, ignore it
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ksu_is_allow_uid(uid))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (ksu_devpts_sid) {
|
||||||
|
struct inode_security_struct *sec = selinux_inode(inode);
|
||||||
|
if (sec) {
|
||||||
|
sec->sid = ksu_devpts_sid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int __ksu_handle_devpts(struct inode *inode)
|
||||||
|
{
|
||||||
|
#ifndef CONFIG_KSU_KPROBES_HOOK
|
||||||
|
if (!ksu_sucompat_non_kp) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return ksu_inline_handle_devpts(inode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// dead code, we are phasing out ksu_handle_devpts for LSM hooks.
|
||||||
|
int __maybe_unused ksu_handle_devpts(struct inode *inode)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
static int faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
static int faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
||||||
{
|
{
|
||||||
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
struct pt_regs *real_regs = PT_REAL_REGS(regs);
|
||||||
@@ -197,6 +263,24 @@ static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
|
|||||||
NULL);
|
NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef MODULE
|
||||||
|
static struct kprobe *su_kps[4];
|
||||||
|
static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs)
|
||||||
|
{
|
||||||
|
struct inode *inode;
|
||||||
|
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0)
|
||||||
|
struct file *file = (struct file *)PT_REGS_PARM2(regs);
|
||||||
|
inode = file->f_path.dentry->d_inode;
|
||||||
|
#else
|
||||||
|
inode = (struct inode *)PT_REGS_PARM2(regs);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return ksu_inline_handle_devpts(inode);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
static struct kprobe *su_kps[3];
|
||||||
|
#endif
|
||||||
|
|
||||||
static struct kprobe *init_kprobe(const char *name,
|
static struct kprobe *init_kprobe(const char *name,
|
||||||
kprobe_pre_handler_t handler)
|
kprobe_pre_handler_t handler)
|
||||||
{
|
{
|
||||||
@@ -227,24 +311,32 @@ static void destroy_kprobe(struct kprobe **kp_ptr)
|
|||||||
*kp_ptr = NULL;
|
*kp_ptr = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
static struct kprobe *su_kps[3];
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// sucompat: permited process can execute 'su' to gain root access.
|
// sucompat: permited process can execute 'su' to gain root access.
|
||||||
void ksu_sucompat_init()
|
void ksu_sucompat_init()
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
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);
|
||||||
|
#ifdef MODULE
|
||||||
|
su_kps[3] = init_kprobe("pts_unix98_lookup", pts_unix98_lookup_pre);
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
ksu_sucompat_non_kp = true;
|
||||||
|
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void ksu_sucompat_exit()
|
void ksu_sucompat_exit()
|
||||||
{
|
{
|
||||||
#ifdef CONFIG_KPROBES
|
#ifdef CONFIG_KSU_KPROBES_HOOK
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include "manager.h"
|
#include "manager.h"
|
||||||
#include "throne_tracker.h"
|
#include "throne_tracker.h"
|
||||||
#include "kernel_compat.h"
|
#include "kernel_compat.h"
|
||||||
|
#include "dynamic_manager.h"
|
||||||
|
|
||||||
uid_t ksu_manager_uid = KSU_INVALID_UID;
|
uid_t ksu_manager_uid = KSU_INVALID_UID;
|
||||||
|
|
||||||
@@ -62,7 +63,7 @@ static int get_pkg_from_apk_path(char *pkg, const char *path)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void crown_manager(const char *apk, struct list_head *uid_data)
|
static void crown_manager(const char *apk, struct list_head *uid_data, int signature_index)
|
||||||
{
|
{
|
||||||
char pkg[KSU_MAX_PACKAGE_NAME];
|
char pkg[KSU_MAX_PACKAGE_NAME];
|
||||||
if (get_pkg_from_apk_path(pkg, apk) < 0) {
|
if (get_pkg_from_apk_path(pkg, apk) < 0) {
|
||||||
@@ -70,7 +71,7 @@ static void crown_manager(const char *apk, struct list_head *uid_data)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
pr_info("manager pkg: %s\n", pkg);
|
pr_info("manager pkg: %s, signature_index: %d\n", pkg, signature_index);
|
||||||
|
|
||||||
#ifdef KSU_MANAGER_PACKAGE
|
#ifdef KSU_MANAGER_PACKAGE
|
||||||
// pkg is `/<real package>`
|
// pkg is `/<real package>`
|
||||||
@@ -85,8 +86,17 @@ static void crown_manager(const char *apk, struct list_head *uid_data)
|
|||||||
|
|
||||||
list_for_each_entry (np, list, list) {
|
list_for_each_entry (np, list, list) {
|
||||||
if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) {
|
if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) {
|
||||||
pr_info("Crowning manager: %s(uid=%d)\n", pkg, np->uid);
|
pr_info("Crowning manager: %s(uid=%d, signature_index=%d)\n", pkg, np->uid, signature_index);
|
||||||
|
|
||||||
|
if (signature_index == 1 || signature_index == 2) {
|
||||||
|
ksu_add_manager(np->uid, signature_index);
|
||||||
|
|
||||||
|
if (!ksu_is_manager_uid_valid()) {
|
||||||
ksu_set_manager_uid(np->uid);
|
ksu_set_manager_uid(np->uid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ksu_set_manager_uid(np->uid);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,11 +195,24 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_manager = is_manager_apk(dirpath);
|
int signature_index = -1;
|
||||||
pr_info("Found new base.apk at path: %s, is_manager: %d\n",
|
bool is_multi_manager = ksu_is_dynamic_manager_apk(dirpath, &signature_index);
|
||||||
dirpath, is_manager);
|
|
||||||
if (is_manager) {
|
pr_info("Found new base.apk at path: %s, is_multi_manager: %d, signature_index: %d\n",
|
||||||
crown_manager(dirpath, my_ctx->private_data);
|
dirpath, is_multi_manager, signature_index);
|
||||||
|
|
||||||
|
if (is_multi_manager && (signature_index == 1 || signature_index == 2)) {
|
||||||
|
crown_manager(dirpath, my_ctx->private_data, signature_index);
|
||||||
|
|
||||||
|
struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC);
|
||||||
|
if (apk_data) {
|
||||||
|
apk_data->hash = hash;
|
||||||
|
apk_data->exists = true;
|
||||||
|
list_add_tail(&apk_data->list, &apk_path_hash_list);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (is_manager_apk(dirpath)) {
|
||||||
|
crown_manager(dirpath, my_ctx->private_data, 0);
|
||||||
*my_ctx->stop = 1;
|
*my_ctx->stop = 1;
|
||||||
|
|
||||||
// Manager found, clear APK cache list
|
// Manager found, clear APK cache list
|
||||||
@@ -199,12 +222,14 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC);
|
struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC);
|
||||||
|
if (apk_data) {
|
||||||
apk_data->hash = hash;
|
apk_data->hash = hash;
|
||||||
apk_data->exists = true;
|
apk_data->exists = true;
|
||||||
list_add_tail(&apk_data->list, &apk_path_hash_list);
|
list_add_tail(&apk_data->list, &apk_path_hash_list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return FILLDIR_ACTOR_CONTINUE;
|
return FILLDIR_ACTOR_CONTINUE;
|
||||||
}
|
}
|
||||||
@@ -214,6 +239,7 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
|
|||||||
int i, stop = 0;
|
int i, stop = 0;
|
||||||
struct list_head data_path_list;
|
struct list_head data_path_list;
|
||||||
INIT_LIST_HEAD(&data_path_list);
|
INIT_LIST_HEAD(&data_path_list);
|
||||||
|
unsigned long data_app_magic = 0;
|
||||||
|
|
||||||
// Initialize APK cache list
|
// Initialize APK cache list
|
||||||
struct apk_path_hash *pos, *n;
|
struct apk_path_hash *pos, *n;
|
||||||
@@ -246,6 +272,24 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
|
|||||||
goto skip_iterate;
|
goto skip_iterate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// grab magic on first folder, which is /data/app
|
||||||
|
if (!data_app_magic) {
|
||||||
|
if (file->f_inode->i_sb->s_magic) {
|
||||||
|
data_app_magic = file->f_inode->i_sb->s_magic;
|
||||||
|
pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, pos->dirpath, data_app_magic);
|
||||||
|
} else {
|
||||||
|
filp_close(file, NULL);
|
||||||
|
goto skip_iterate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file->f_inode->i_sb->s_magic != data_app_magic) {
|
||||||
|
pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", __func__, pos->dirpath,
|
||||||
|
file->f_inode->i_sb->s_magic, data_app_magic);
|
||||||
|
filp_close(file, NULL);
|
||||||
|
goto skip_iterate;
|
||||||
|
}
|
||||||
|
|
||||||
iterate_dir(file, &ctx.ctx);
|
iterate_dir(file, &ctx.ctx);
|
||||||
filp_close(file, NULL);
|
filp_close(file, NULL);
|
||||||
}
|
}
|
||||||
@@ -344,6 +388,8 @@ void track_throne()
|
|||||||
|
|
||||||
// first, check if manager_uid exist!
|
// first, check if manager_uid exist!
|
||||||
bool manager_exist = false;
|
bool manager_exist = false;
|
||||||
|
bool dynamic_manager_exist = false;
|
||||||
|
|
||||||
list_for_each_entry (np, &uid_list, list) {
|
list_for_each_entry (np, &uid_list, list) {
|
||||||
// if manager is installed in work profile, the uid in packages.list is still equals main profile
|
// if manager is installed in work profile, the uid in packages.list is still equals main profile
|
||||||
// don't delete it in this case!
|
// don't delete it in this case!
|
||||||
@@ -354,16 +400,33 @@ void track_throne()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for dynamic managers
|
||||||
|
if (!dynamic_manager_exist && ksu_is_dynamic_manager_enabled()) {
|
||||||
|
list_for_each_entry (np, &uid_list, list) {
|
||||||
|
if (ksu_is_any_manager(np->uid)) {
|
||||||
|
dynamic_manager_exist = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!manager_exist) {
|
if (!manager_exist) {
|
||||||
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");
|
||||||
|
} else if (!dynamic_manager_exist && ksu_is_dynamic_manager_enabled()) {
|
||||||
|
// Always perform search when called from dynamic manager rescan
|
||||||
|
pr_info("Dynamic sign enabled, Searching manager...\n");
|
||||||
|
search_manager("/data/app", 2, &uid_list);
|
||||||
|
pr_info("Search Dynamic sign 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:
|
||||||
|
|||||||
@@ -24,7 +24,17 @@ apksign {
|
|||||||
keyPasswordProperty = "KEY_PASSWORD"
|
keyPasswordProperty = "KEY_PASSWORD"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
/**signingConfigs {
|
||||||
|
create("Debug") {
|
||||||
|
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
|
||||||
|
storePassword = ""
|
||||||
|
keyAlias = ""
|
||||||
|
keyPassword = ""
|
||||||
|
}
|
||||||
|
}**/
|
||||||
namespace = "com.sukisu.ultra"
|
namespace = "com.sukisu.ultra"
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -33,10 +43,12 @@ 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 {
|
||||||
aidl = true
|
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
prefab = true
|
prefab = true
|
||||||
@@ -94,7 +106,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("compose-destinations.defaultTransitions", "none")
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation(libs.gson)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
|
||||||
@@ -140,4 +157,11 @@ 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)
|
||||||
|
|
||||||
|
implementation(libs.accompanist.drawablepainter)
|
||||||
|
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -30,6 +32,19 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity-alias
|
||||||
|
android:name=".ui.MainActivityAlias"
|
||||||
|
android:exported="true"
|
||||||
|
android:enabled="false"
|
||||||
|
android:icon="@mipmap/ic_launcher_alt"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_alt_round"
|
||||||
|
android:targetActivity=".ui.MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.webui.WebUIActivity"
|
android:name=".ui.webui.WebUIActivity"
|
||||||
android:autoRemoveFromRecents="true"
|
android:autoRemoveFromRecents="true"
|
||||||
@@ -37,6 +52,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,8 +0,0 @@
|
|||||||
package com.sukisu.zako;
|
|
||||||
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
|
||||||
|
|
||||||
interface IKsuInterface {
|
|
||||||
ParcelableListSlice<PackageInfo> getPackages(int flags);
|
|
||||||
}
|
|
||||||
BIN
manager/app/src/main/assets/5_10-mkbootfs
Normal file
BIN
manager/app/src/main/assets/5_10-mkbootfs
Normal file
Binary file not shown.
BIN
manager/app/src/main/assets/ksu_susfs_1.5.7
Normal file
BIN
manager/app/src/main/assets/ksu_susfs_1.5.7
Normal file
Binary file not shown.
BIN
manager/app/src/main/assets/ksu_susfs_1.5.8
Normal file
BIN
manager/app/src/main/assets/ksu_susfs_1.5.8
Normal file
Binary file not shown.
BIN
manager/app/src/main/assets/ksu_susfs_1.5.9
Normal file
BIN
manager/app/src/main/assets/ksu_susfs_1.5.9
Normal file
Binary file not shown.
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
# For more information about using CMake with Android Studio, read the
|
# For more information about using CMake with Android Studio, read the
|
||||||
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||||
|
|
||||||
@@ -7,15 +6,22 @@ cmake_minimum_required(VERSION 3.18.1)
|
|||||||
|
|
||||||
project("kernelsu")
|
project("kernelsu")
|
||||||
|
|
||||||
find_package(cxx REQUIRED CONFIG)
|
|
||||||
link_libraries(cxx::cxx)
|
|
||||||
|
|
||||||
add_library(zako
|
add_library(zako
|
||||||
SHARED
|
SHARED
|
||||||
jni.cc
|
jni.c
|
||||||
ksu.cc
|
ksu.c
|
||||||
)
|
)
|
||||||
|
|
||||||
find_library(log-lib log)
|
find_library(log-lib log)
|
||||||
|
|
||||||
target_link_libraries(zako ${log-lib})
|
if(ANDROID_ABI STREQUAL "arm64-v8a")
|
||||||
|
set(zakosign-lib ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libzakosign.so)
|
||||||
|
elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
|
||||||
|
set(zakosign-lib ${CMAKE_SOURCE_DIR}/../jniLibs/armeabi-v7a/libzakosign.so)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ANDROID_ABI STREQUAL "arm64-v8a" OR ANDROID_ABI STREQUAL "armeabi-v7a")
|
||||||
|
target_link_libraries(zako ${log-lib} ${zakosign-lib})
|
||||||
|
else()
|
||||||
|
target_link_libraries(zako ${log-lib})
|
||||||
|
endif()
|
||||||
|
|||||||
434
manager/app/src/main/cpp/jni.c
Normal file
434
manager/app/src/main/cpp/jni.c
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
#include "prelude.h"
|
||||||
|
#include "ksu.h"
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
|
||||||
|
NativeBridge(becomeManager, jboolean, jstring pkg) {
|
||||||
|
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, JNI_FALSE);
|
||||||
|
bool result = become_manager(cpkg);
|
||||||
|
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(getVersion, jint) {
|
||||||
|
return get_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
// get VERSION FULL
|
||||||
|
NativeBridgeNP(getFullVersion, jstring) {
|
||||||
|
char buff[255] = { 0 };
|
||||||
|
get_full_version((char *) &buff);
|
||||||
|
return GetEnvironment()->NewStringUTF(env, buff);
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(getAllowList, jintArray) {
|
||||||
|
int uids[1024];
|
||||||
|
int size = 0;
|
||||||
|
bool result = get_allow_list(uids, &size);
|
||||||
|
|
||||||
|
LogDebug("getAllowList: %d, size: %d", result, size);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
jintArray array = GetEnvironment()->NewIntArray(env, size);
|
||||||
|
GetEnvironment()->SetIntArrayRegion(env, array, 0, size, uids);
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetEnvironment()->NewIntArray(env, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(isSafeMode, jboolean) {
|
||||||
|
return is_safe_mode();
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(isLkmMode, jboolean) {
|
||||||
|
return is_lkm_mode();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
|
||||||
|
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||||
|
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
|
||||||
|
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]);
|
||||||
|
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void addIntToList(JNIEnv *env, jobject list, int ele) {
|
||||||
|
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||||
|
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
|
||||||
|
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
|
||||||
|
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele);
|
||||||
|
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint64_t capListToBits(JNIEnv *env, jobject list) {
|
||||||
|
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||||
|
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
|
||||||
|
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
|
||||||
|
jint listSize = GetEnvironment()->CallIntMethod(env, list, size);
|
||||||
|
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||||
|
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
|
||||||
|
uint64_t result = 0;
|
||||||
|
for (int i = 0; i < listSize; ++i) {
|
||||||
|
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
|
||||||
|
int data = GetEnvironment()->CallIntMethod(env, integer, intValue);
|
||||||
|
|
||||||
|
if (cap_valid(data)) {
|
||||||
|
result |= (1ULL << data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int getListSize(JNIEnv *env, jobject list) {
|
||||||
|
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||||
|
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
|
||||||
|
return GetEnvironment()->CallIntMethod(env, list, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
||||||
|
jclass cls = GetEnvironment()->GetObjectClass(env, list);
|
||||||
|
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
|
||||||
|
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
|
||||||
|
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
|
||||||
|
data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridge(getAppProfile, jobject, jstring pkg, jint uid) {
|
||||||
|
if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
char key[KSU_MAX_PACKAGE_NAME] = { 0 };
|
||||||
|
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr);
|
||||||
|
strcpy(key, cpkg);
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
|
||||||
|
|
||||||
|
struct app_profile profile = { 0 };
|
||||||
|
profile.version = KSU_APP_PROFILE_VER;
|
||||||
|
|
||||||
|
strcpy(profile.key, key);
|
||||||
|
profile.current_uid = uid;
|
||||||
|
|
||||||
|
bool useDefaultProfile = !get_app_profile(key, &profile);
|
||||||
|
|
||||||
|
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
|
||||||
|
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
|
||||||
|
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
|
||||||
|
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
|
||||||
|
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
|
||||||
|
|
||||||
|
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
|
||||||
|
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
|
||||||
|
|
||||||
|
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
|
||||||
|
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
|
||||||
|
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
|
||||||
|
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
|
||||||
|
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
|
||||||
|
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
|
||||||
|
|
||||||
|
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
|
||||||
|
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
|
||||||
|
|
||||||
|
GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key));
|
||||||
|
GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid);
|
||||||
|
|
||||||
|
if (useDefaultProfile) {
|
||||||
|
// no profile found, so just use default profile:
|
||||||
|
// don't allow root and use default profile!
|
||||||
|
LogDebug("use default profile for: %s, %d", key, uid);
|
||||||
|
|
||||||
|
// allow_su = false
|
||||||
|
// non root use default = true
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, allowSuField, false);
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allowSu = profile.allow_su;
|
||||||
|
|
||||||
|
if (allowSu) {
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
|
||||||
|
if (strlen(profile.rp_config.template_name) > 0) {
|
||||||
|
GetEnvironment()->SetObjectField(env, obj, rootTemplateField,
|
||||||
|
GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name));
|
||||||
|
}
|
||||||
|
|
||||||
|
GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid);
|
||||||
|
GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid);
|
||||||
|
|
||||||
|
jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField);
|
||||||
|
int groupCount = profile.rp_config.profile.groups_count;
|
||||||
|
if (groupCount > KSU_MAX_GROUPS) {
|
||||||
|
LogDebug("kernel group count too large: %d???", groupCount);
|
||||||
|
groupCount = KSU_MAX_GROUPS;
|
||||||
|
}
|
||||||
|
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
|
||||||
|
|
||||||
|
jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField);
|
||||||
|
for (int i = 0; i <= CAP_LAST_CAP; i++) {
|
||||||
|
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
|
||||||
|
addIntToList(env, capList, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GetEnvironment()->SetObjectField(env, obj, domainField,
|
||||||
|
GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain));
|
||||||
|
GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces);
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su);
|
||||||
|
} else {
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default);
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridge(setAppProfile, jboolean, jobject profile) {
|
||||||
|
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
|
||||||
|
|
||||||
|
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
|
||||||
|
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
|
||||||
|
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
|
||||||
|
|
||||||
|
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
|
||||||
|
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
|
||||||
|
|
||||||
|
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
|
||||||
|
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
|
||||||
|
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
|
||||||
|
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
|
||||||
|
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
|
||||||
|
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
|
||||||
|
|
||||||
|
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
|
||||||
|
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
|
||||||
|
|
||||||
|
jobject key = GetEnvironment()->GetObjectField(env, profile, keyField);
|
||||||
|
if (!key) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr);
|
||||||
|
char p_key[KSU_MAX_PACKAGE_NAME] = { 0 };
|
||||||
|
strcpy(p_key, cpkg);
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg);
|
||||||
|
|
||||||
|
jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField);
|
||||||
|
|
||||||
|
jint uid = GetEnvironment()->GetIntField(env, profile, uidField);
|
||||||
|
jint gid = GetEnvironment()->GetIntField(env, profile, gidField);
|
||||||
|
jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField);
|
||||||
|
jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField);
|
||||||
|
jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField);
|
||||||
|
jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField);
|
||||||
|
jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField);
|
||||||
|
|
||||||
|
struct app_profile p = { 0 };
|
||||||
|
p.version = KSU_APP_PROFILE_VER;
|
||||||
|
|
||||||
|
strcpy(p.key, p_key);
|
||||||
|
p.allow_su = allowSu;
|
||||||
|
p.current_uid = currentUid;
|
||||||
|
|
||||||
|
if (allowSu) {
|
||||||
|
p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField);
|
||||||
|
jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField);
|
||||||
|
if (templateName) {
|
||||||
|
const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr);
|
||||||
|
strcpy(p.rp_config.template_name, ctemplateName);
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.rp_config.profile.uid = uid;
|
||||||
|
p.rp_config.profile.gid = gid;
|
||||||
|
|
||||||
|
int groups_count = getListSize(env, groups);
|
||||||
|
if (groups_count > KSU_MAX_GROUPS) {
|
||||||
|
LogDebug("groups count too large: %d", groups_count);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
p.rp_config.profile.groups_count = groups_count;
|
||||||
|
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
|
||||||
|
|
||||||
|
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
|
||||||
|
|
||||||
|
const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr);
|
||||||
|
strcpy(p.rp_config.profile.selinux_domain, cdomain);
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain);
|
||||||
|
|
||||||
|
p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField);
|
||||||
|
} else {
|
||||||
|
p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField);
|
||||||
|
p.nrp_config.profile.umount_modules = umountModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
return set_app_profile(&p);
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridge(uidShouldUmount, jboolean, jint uid) {
|
||||||
|
return uid_should_umount(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(isSuEnabled, jboolean) {
|
||||||
|
return is_su_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridge(setSuEnabled, jboolean, jboolean enabled) {
|
||||||
|
return set_su_enabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if KPM is enabled
|
||||||
|
NativeBridgeNP(isKPMEnabled, jboolean) {
|
||||||
|
return is_KPM_enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get HOOK type
|
||||||
|
NativeBridgeNP(getHookType, jstring) {
|
||||||
|
char hook_type[16];
|
||||||
|
get_hook_type(hook_type, sizeof(hook_type));
|
||||||
|
return GetEnvironment()->NewStringUTF(env, hook_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuSFS Related Function Status
|
||||||
|
NativeBridgeNP(getSusfsFeatureStatus, jobject) {
|
||||||
|
struct susfs_feature_status status;
|
||||||
|
bool result = get_susfs_feature_status(&status);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$SusfsFeatureStatus");
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
|
||||||
|
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
|
||||||
|
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusSusPath, status.status_sus_path);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusSusMount, status.status_sus_mount);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusAutoDefaultMount, status.status_auto_default_mount);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusAutoBindMount, status.status_auto_bind_mount);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusSusKstat, status.status_sus_kstat);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusTryUmount, status.status_try_umount);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusAutoTryUmountBind, status.status_auto_try_umount_bind);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusSpoofUname, status.status_spoof_uname);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusEnableLog, status.status_enable_log);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusHideSymbols, status.status_hide_symbols);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusSpoofCmdline, status.status_spoof_cmdline);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusOpenRedirect, status.status_open_redirect);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusMagicMount, status.status_magic_mount);
|
||||||
|
SET_BOOLEAN_FIELD(obj, cls, statusSusSu, status.status_sus_su);
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dynamic manager
|
||||||
|
NativeBridge(setDynamicManager, jboolean, jint size, jstring hash) {
|
||||||
|
if (!hash) {
|
||||||
|
LogDebug("setDynamicManager: hash is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr);
|
||||||
|
bool result = set_dynamic_manager((unsigned int)size, chash);
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, hash, chash);
|
||||||
|
|
||||||
|
LogDebug("setDynamicManager: size=0x%x, result=%d", size, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(getDynamicManager, jobject) {
|
||||||
|
struct dynamic_manager_user_config config;
|
||||||
|
bool result = get_dynamic_manager(&config);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
LogDebug("getDynamicManager: failed to get dynamic manager config");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig");
|
||||||
|
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig");
|
||||||
|
|
||||||
|
SET_INT_FIELD(obj, cls, size, (jint)config.size);
|
||||||
|
SET_STRING_FIELD(obj, cls, hash, config.hash);
|
||||||
|
|
||||||
|
LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridgeNP(clearDynamicManager, jboolean) {
|
||||||
|
bool result = clear_dynamic_manager();
|
||||||
|
LogDebug("clearDynamicManager: result=%d", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a list of active managers
|
||||||
|
NativeBridgeNP(getManagersList, jobject) {
|
||||||
|
struct manager_list_info managerListInfo;
|
||||||
|
bool result = get_managers_list(&managerListInfo);
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
LogDebug("getManagersList: failed to get active managers list");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList");
|
||||||
|
jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList");
|
||||||
|
|
||||||
|
SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count);
|
||||||
|
|
||||||
|
jobject managersList = CREATE_ARRAYLIST();
|
||||||
|
|
||||||
|
for (int i = 0; i < managerListInfo.count; i++) {
|
||||||
|
jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS(
|
||||||
|
"com/sukisu/ultra/Natives$ManagerInfo",
|
||||||
|
"(II)V",
|
||||||
|
(jint)managerListInfo.managers[i].uid,
|
||||||
|
(jint)managerListInfo.managers[i].signature_index
|
||||||
|
);
|
||||||
|
ADD_TO_LIST(managersList, managerInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
SET_OBJECT_FIELD(obj, managerListCls, managers, managersList);
|
||||||
|
|
||||||
|
LogDebug("getManagersList: count=%d", managerListInfo.count);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) {
|
||||||
|
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
|
||||||
|
if (!modulePath) {
|
||||||
|
LogDebug("verifyModuleSignature: modulePath is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr);
|
||||||
|
bool result = verify_module_signature(cModulePath);
|
||||||
|
GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath);
|
||||||
|
|
||||||
|
LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result);
|
||||||
|
return result;
|
||||||
|
#else
|
||||||
|
LogDebug("verifyModuleSignature: not supported on non-ARM architecture");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#include <sys/prctl.h>
|
|
||||||
|
|
||||||
#include <android/log.h>
|
|
||||||
#include <cstring>
|
|
||||||
|
|
||||||
#include "ksu.h"
|
|
||||||
|
|
||||||
#define LOG_TAG "KernelSU"
|
|
||||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
|
|
||||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
|
||||||
auto result = become_manager(cpkg);
|
|
||||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jint JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
|
|
||||||
return get_version();
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jintArray JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
|
|
||||||
int uids[1024];
|
|
||||||
int size = 0;
|
|
||||||
bool result = get_allow_list(uids, &size);
|
|
||||||
LOGD("getAllowList: %d, size: %d", result, size);
|
|
||||||
if (result) {
|
|
||||||
auto array = env->NewIntArray(size);
|
|
||||||
env->SetIntArrayRegion(array, 0, size, uids);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
return env->NewIntArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
|
|
||||||
return is_safe_mode();
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
|
|
||||||
return is_lkm_mode();
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
|
|
||||||
auto cls = env->GetObjectClass(list);
|
|
||||||
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
|
|
||||||
auto integerCls = env->FindClass("java/lang/Integer");
|
|
||||||
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
auto integer = env->NewObject(integerCls, constructor, data[i]);
|
|
||||||
env->CallBooleanMethod(list, add, integer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void addIntToList(JNIEnv *env, jobject list, int ele) {
|
|
||||||
auto cls = env->GetObjectClass(list);
|
|
||||||
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
|
|
||||||
auto integerCls = env->FindClass("java/lang/Integer");
|
|
||||||
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
|
|
||||||
auto integer = env->NewObject(integerCls, constructor, ele);
|
|
||||||
env->CallBooleanMethod(list, add, integer);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint64_t capListToBits(JNIEnv *env, jobject list) {
|
|
||||||
auto cls = env->GetObjectClass(list);
|
|
||||||
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
|
|
||||||
auto size = env->GetMethodID(cls, "size", "()I");
|
|
||||||
auto listSize = env->CallIntMethod(list, size);
|
|
||||||
auto integerCls = env->FindClass("java/lang/Integer");
|
|
||||||
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
|
|
||||||
uint64_t result = 0;
|
|
||||||
for (int i = 0; i < listSize; ++i) {
|
|
||||||
auto integer = env->CallObjectMethod(list, get, i);
|
|
||||||
int data = env->CallIntMethod(integer, intValue);
|
|
||||||
|
|
||||||
if (cap_valid(data)) {
|
|
||||||
result |= (1ULL << data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int getListSize(JNIEnv *env, jobject list) {
|
|
||||||
auto cls = env->GetObjectClass(list);
|
|
||||||
auto size = env->GetMethodID(cls, "size", "()I");
|
|
||||||
return env->CallIntMethod(list, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
|
|
||||||
auto cls = env->GetObjectClass(list);
|
|
||||||
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
|
|
||||||
auto integerCls = env->FindClass("java/lang/Integer");
|
|
||||||
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
|
|
||||||
for (int i = 0; i < count; ++i) {
|
|
||||||
auto integer = env->CallObjectMethod(list, get, i);
|
|
||||||
data[i] = env->CallIntMethod(integer, intValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jobject JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
|
|
||||||
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
p_key_t key = {};
|
|
||||||
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
|
|
||||||
strcpy(key, cpkg);
|
|
||||||
env->ReleaseStringUTFChars(pkg, cpkg);
|
|
||||||
|
|
||||||
app_profile profile = {};
|
|
||||||
profile.version = KSU_APP_PROFILE_VER;
|
|
||||||
|
|
||||||
strcpy(profile.key, key);
|
|
||||||
profile.current_uid = uid;
|
|
||||||
|
|
||||||
bool useDefaultProfile = !get_app_profile(key, &profile);
|
|
||||||
|
|
||||||
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
|
||||||
auto constructor = env->GetMethodID(cls, "<init>", "()V");
|
|
||||||
auto obj = env->NewObject(cls, constructor);
|
|
||||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
|
||||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
|
||||||
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
|
|
||||||
|
|
||||||
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
|
|
||||||
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
|
|
||||||
|
|
||||||
auto uidField = env->GetFieldID(cls, "uid", "I");
|
|
||||||
auto gidField = env->GetFieldID(cls, "gid", "I");
|
|
||||||
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
|
|
||||||
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
|
|
||||||
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
|
|
||||||
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
|
|
||||||
|
|
||||||
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
|
|
||||||
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
|
|
||||||
|
|
||||||
env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key));
|
|
||||||
env->SetIntField(obj, currentUidField, profile.current_uid);
|
|
||||||
|
|
||||||
if (useDefaultProfile) {
|
|
||||||
// no profile found, so just use default profile:
|
|
||||||
// don't allow root and use default profile!
|
|
||||||
LOGD("use default profile for: %s, %d", key, uid);
|
|
||||||
|
|
||||||
// allow_su = false
|
|
||||||
// non root use default = true
|
|
||||||
env->SetBooleanField(obj, allowSuField, false);
|
|
||||||
env->SetBooleanField(obj, nonRootUseDefaultField, true);
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto allowSu = profile.allow_su;
|
|
||||||
|
|
||||||
if (allowSu) {
|
|
||||||
env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
|
|
||||||
if (strlen(profile.rp_config.template_name) > 0) {
|
|
||||||
env->SetObjectField(obj, rootTemplateField,
|
|
||||||
env->NewStringUTF(profile.rp_config.template_name));
|
|
||||||
}
|
|
||||||
|
|
||||||
env->SetIntField(obj, uidField, profile.rp_config.profile.uid);
|
|
||||||
env->SetIntField(obj, gidField, profile.rp_config.profile.gid);
|
|
||||||
|
|
||||||
jobject groupList = env->GetObjectField(obj, groupsField);
|
|
||||||
int groupCount = profile.rp_config.profile.groups_count;
|
|
||||||
if (groupCount > KSU_MAX_GROUPS) {
|
|
||||||
LOGD("kernel group count too large: %d???", groupCount);
|
|
||||||
groupCount = KSU_MAX_GROUPS;
|
|
||||||
}
|
|
||||||
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
|
|
||||||
|
|
||||||
jobject capList = env->GetObjectField(obj, capabilitiesField);
|
|
||||||
for (int i = 0; i <= CAP_LAST_CAP; i++) {
|
|
||||||
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
|
|
||||||
addIntToList(env, capList, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
env->SetObjectField(obj, domainField,
|
|
||||||
env->NewStringUTF(profile.rp_config.profile.selinux_domain));
|
|
||||||
env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces);
|
|
||||||
env->SetBooleanField(obj, allowSuField, profile.allow_su);
|
|
||||||
} else {
|
|
||||||
env->SetBooleanField(obj, nonRootUseDefaultField,
|
|
||||||
(jboolean) profile.nrp_config.use_default);
|
|
||||||
env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules);
|
|
||||||
}
|
|
||||||
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
|
|
||||||
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
|
|
||||||
|
|
||||||
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
|
|
||||||
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
|
|
||||||
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
|
|
||||||
|
|
||||||
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
|
|
||||||
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
|
|
||||||
|
|
||||||
auto uidField = env->GetFieldID(cls, "uid", "I");
|
|
||||||
auto gidField = env->GetFieldID(cls, "gid", "I");
|
|
||||||
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
|
|
||||||
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
|
|
||||||
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
|
|
||||||
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
|
|
||||||
|
|
||||||
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
|
|
||||||
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
|
|
||||||
|
|
||||||
auto key = env->GetObjectField(profile, keyField);
|
|
||||||
if (!key) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto cpkg = env->GetStringUTFChars((jstring) key, nullptr);
|
|
||||||
p_key_t p_key = {};
|
|
||||||
strcpy(p_key, cpkg);
|
|
||||||
env->ReleaseStringUTFChars((jstring) key, cpkg);
|
|
||||||
|
|
||||||
auto currentUid = env->GetIntField(profile, currentUidField);
|
|
||||||
|
|
||||||
auto uid = env->GetIntField(profile, uidField);
|
|
||||||
auto gid = env->GetIntField(profile, gidField);
|
|
||||||
auto groups = env->GetObjectField(profile, groupsField);
|
|
||||||
auto capabilities = env->GetObjectField(profile, capabilitiesField);
|
|
||||||
auto domain = env->GetObjectField(profile, domainField);
|
|
||||||
auto allowSu = env->GetBooleanField(profile, allowSuField);
|
|
||||||
auto umountModules = env->GetBooleanField(profile, umountModulesField);
|
|
||||||
|
|
||||||
app_profile p = {};
|
|
||||||
p.version = KSU_APP_PROFILE_VER;
|
|
||||||
|
|
||||||
strcpy(p.key, p_key);
|
|
||||||
p.allow_su = allowSu;
|
|
||||||
p.current_uid = currentUid;
|
|
||||||
|
|
||||||
if (allowSu) {
|
|
||||||
p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField);
|
|
||||||
auto templateName = env->GetObjectField(profile, rootTemplateField);
|
|
||||||
if (templateName) {
|
|
||||||
auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr);
|
|
||||||
strcpy(p.rp_config.template_name, ctemplateName);
|
|
||||||
env->ReleaseStringUTFChars((jstring) templateName, ctemplateName);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.rp_config.profile.uid = uid;
|
|
||||||
p.rp_config.profile.gid = gid;
|
|
||||||
|
|
||||||
int groups_count = getListSize(env, groups);
|
|
||||||
if (groups_count > KSU_MAX_GROUPS) {
|
|
||||||
LOGD("groups count too large: %d", groups_count);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
p.rp_config.profile.groups_count = groups_count;
|
|
||||||
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
|
|
||||||
|
|
||||||
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
|
|
||||||
|
|
||||||
auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr);
|
|
||||||
strcpy(p.rp_config.profile.selinux_domain, cdomain);
|
|
||||||
env->ReleaseStringUTFChars((jstring) domain, cdomain);
|
|
||||||
|
|
||||||
p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField);
|
|
||||||
} else {
|
|
||||||
p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField);
|
|
||||||
p.nrp_config.profile.umount_modules = umountModules;
|
|
||||||
}
|
|
||||||
|
|
||||||
return set_app_profile(&p);
|
|
||||||
}
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
|
|
||||||
return uid_should_umount(uid);
|
|
||||||
}
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
|
|
||||||
return is_su_enabled();
|
|
||||||
}
|
|
||||||
extern "C"
|
|
||||||
JNIEXPORT jboolean JNICALL
|
|
||||||
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
|
|
||||||
return set_su_enabled(enabled);
|
|
||||||
}
|
|
||||||
252
manager/app/src/main/cpp/ksu.c
Normal file
252
manager/app/src/main/cpp/ksu.c
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
//
|
||||||
|
// Created by weishu on 2022/12/9.
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <sys/prctl.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "prelude.h"
|
||||||
|
#include "ksu.h"
|
||||||
|
|
||||||
|
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
|
||||||
|
|
||||||
|
// Zako extern declarations
|
||||||
|
#define ZAKO_ESV_IMPORTANT_ERROR 1 << 31
|
||||||
|
extern int zako_sys_file_open(const char* path);
|
||||||
|
extern uint32_t zako_file_verify_esig(int fd, uint32_t flags);
|
||||||
|
extern const char* zako_file_verrcidx2str(uint8_t index);
|
||||||
|
|
||||||
|
#endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM
|
||||||
|
|
||||||
|
#define KERNEL_SU_OPTION 0xDEADBEEF
|
||||||
|
|
||||||
|
#define CMD_GRANT_ROOT 0
|
||||||
|
|
||||||
|
#define CMD_BECOME_MANAGER 1
|
||||||
|
#define CMD_GET_VERSION 2
|
||||||
|
#define CMD_ALLOW_SU 3
|
||||||
|
#define CMD_DENY_SU 4
|
||||||
|
#define CMD_GET_SU_LIST 5
|
||||||
|
#define CMD_GET_DENY_LIST 6
|
||||||
|
#define CMD_CHECK_SAFEMODE 9
|
||||||
|
|
||||||
|
#define CMD_GET_APP_PROFILE 10
|
||||||
|
#define CMD_SET_APP_PROFILE 11
|
||||||
|
|
||||||
|
#define CMD_IS_UID_GRANTED_ROOT 12
|
||||||
|
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
||||||
|
#define CMD_IS_SU_ENABLED 14
|
||||||
|
#define CMD_ENABLE_SU 15
|
||||||
|
|
||||||
|
#define CMD_GET_VERSION_FULL 0xC0FFEE1A
|
||||||
|
|
||||||
|
#define CMD_ENABLE_KPM 100
|
||||||
|
#define CMD_HOOK_TYPE 101
|
||||||
|
#define CMD_GET_SUSFS_FEATURE_STATUS 102
|
||||||
|
#define CMD_DYNAMIC_MANAGER 103
|
||||||
|
#define CMD_GET_MANAGERS 104
|
||||||
|
|
||||||
|
#define DYNAMIC_MANAGER_OP_SET 0
|
||||||
|
#define DYNAMIC_MANAGER_OP_GET 1
|
||||||
|
#define DYNAMIC_MANAGER_OP_CLEAR 2
|
||||||
|
|
||||||
|
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
||||||
|
int32_t result = 0;
|
||||||
|
int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
|
||||||
|
|
||||||
|
return result == KERNEL_SU_OPTION && rtn == -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool become_manager(const char* pkg) {
|
||||||
|
char param[128];
|
||||||
|
uid_t uid = getuid();
|
||||||
|
uint32_t userId = uid / 100000;
|
||||||
|
if (userId == 0) {
|
||||||
|
sprintf(param, "/data/data/%s", pkg);
|
||||||
|
} else {
|
||||||
|
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ksuctl(CMD_BECOME_MANAGER, param, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cache the result to avoid unnecessary syscall
|
||||||
|
static bool is_lkm;
|
||||||
|
int get_version() {
|
||||||
|
int32_t version = -1;
|
||||||
|
int32_t flags = 0;
|
||||||
|
ksuctl(CMD_GET_VERSION, &version, &flags);
|
||||||
|
if (!is_lkm && (flags & 0x1)) {
|
||||||
|
is_lkm = true;
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_full_version(char* buff) {
|
||||||
|
ksuctl(CMD_GET_VERSION_FULL, buff, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_allow_list(int *uids, int *size) {
|
||||||
|
return ksuctl(CMD_GET_SU_LIST, uids, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_safe_mode() {
|
||||||
|
return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_lkm_mode() {
|
||||||
|
// you should call get_version first!
|
||||||
|
return is_lkm;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool uid_should_umount(int uid) {
|
||||||
|
int should;
|
||||||
|
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool set_app_profile(const struct app_profile* profile) {
|
||||||
|
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_app_profile(char* key, struct app_profile* profile) {
|
||||||
|
return ksuctl(CMD_GET_APP_PROFILE, profile, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool set_su_enabled(bool enabled) {
|
||||||
|
return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_su_enabled() {
|
||||||
|
int enabled = true;
|
||||||
|
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
|
||||||
|
ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_KPM_enable() {
|
||||||
|
int enabled = false;
|
||||||
|
ksuctl(CMD_ENABLE_KPM, &enabled, NULL);
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_hook_type(char* hook_type, size_t size) {
|
||||||
|
if (hook_type == NULL || size == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char cached_hook_type[16] = {0};
|
||||||
|
if (cached_hook_type[0] == '\0') {
|
||||||
|
if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) {
|
||||||
|
strcpy(cached_hook_type, "Unknown");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strncpy(hook_type, cached_hook_type, size);
|
||||||
|
hook_type[size - 1] = '\0';
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_susfs_feature_status(struct susfs_feature_status* status) {
|
||||||
|
if (status == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ksuctl(CMD_GET_SUSFS_FEATURE_STATUS, status, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool set_dynamic_manager(unsigned int size, const char* hash) {
|
||||||
|
if (hash == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dynamic_manager_user_config config;
|
||||||
|
config.operation = DYNAMIC_MANAGER_OP_SET;
|
||||||
|
config.size = size;
|
||||||
|
strncpy(config.hash, hash, sizeof(config.hash) - 1);
|
||||||
|
config.hash[sizeof(config.hash) - 1] = '\0';
|
||||||
|
|
||||||
|
return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_dynamic_manager(struct dynamic_manager_user_config* config) {
|
||||||
|
if (config == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
config->operation = DYNAMIC_MANAGER_OP_GET;
|
||||||
|
return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool clear_dynamic_manager() {
|
||||||
|
struct dynamic_manager_user_config config;
|
||||||
|
config.operation = DYNAMIC_MANAGER_OP_CLEAR;
|
||||||
|
return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_managers_list(struct manager_list_info* info) {
|
||||||
|
if (info == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ksuctl(CMD_GET_MANAGERS, info, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool verify_module_signature(const char* input) {
|
||||||
|
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
|
||||||
|
if (input == NULL) {
|
||||||
|
LogDebug("verify_module_signature: input path is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = zako_sys_file_open(input);
|
||||||
|
if (fd < 0) {
|
||||||
|
LogDebug("verify_module_signature: failed to open file: %s", input);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t results = zako_file_verify_esig(fd, 0);
|
||||||
|
|
||||||
|
if (results != 0) {
|
||||||
|
/* If important error occured, verification process should
|
||||||
|
be considered as failed due to unexpected modification
|
||||||
|
potentially happened. */
|
||||||
|
if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) {
|
||||||
|
LogDebug("verify_module_signature: Verification failed! (important error)");
|
||||||
|
} else {
|
||||||
|
/* This is for manager that doesn't want to do certificate checks */
|
||||||
|
LogDebug("verify_module_signature: Verification partially passed");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LogDebug("verify_module_signature: Verification passed!");
|
||||||
|
goto exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Go through all bit fields */
|
||||||
|
for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) {
|
||||||
|
if ((results & (1 << i)) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Convert error bit field index into human readable string */
|
||||||
|
const char* message = zako_file_verrcidx2str((uint8_t)i);
|
||||||
|
// Error message: message
|
||||||
|
if (message != NULL) {
|
||||||
|
LogDebug("verify_module_signature: Error bit %zu: %s", i, message);
|
||||||
|
} else {
|
||||||
|
LogDebug("verify_module_signature: Error bit %zu: Unknown error", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exit:
|
||||||
|
close(fd);
|
||||||
|
LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s",
|
||||||
|
input, results, (results == 0) ? "true" : "false");
|
||||||
|
return results == 0;
|
||||||
|
#else
|
||||||
|
LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null");
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by weishu on 2022/12/9.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include <sys/prctl.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "ksu.h"
|
|
||||||
|
|
||||||
#define KERNEL_SU_OPTION 0xDEADBEEF
|
|
||||||
|
|
||||||
#define CMD_GRANT_ROOT 0
|
|
||||||
|
|
||||||
#define CMD_BECOME_MANAGER 1
|
|
||||||
#define CMD_GET_VERSION 2
|
|
||||||
#define CMD_ALLOW_SU 3
|
|
||||||
#define CMD_DENY_SU 4
|
|
||||||
#define CMD_GET_SU_LIST 5
|
|
||||||
#define CMD_GET_DENY_LIST 6
|
|
||||||
#define CMD_CHECK_SAFEMODE 9
|
|
||||||
|
|
||||||
#define CMD_GET_APP_PROFILE 10
|
|
||||||
#define CMD_SET_APP_PROFILE 11
|
|
||||||
|
|
||||||
#define CMD_IS_UID_GRANTED_ROOT 12
|
|
||||||
#define CMD_IS_UID_SHOULD_UMOUNT 13
|
|
||||||
#define CMD_IS_SU_ENABLED 14
|
|
||||||
#define CMD_ENABLE_SU 15
|
|
||||||
|
|
||||||
static bool ksuctl(int cmd, void* arg1, void* arg2) {
|
|
||||||
int32_t result = 0;
|
|
||||||
prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
|
|
||||||
return result == KERNEL_SU_OPTION;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool become_manager(const char* pkg) {
|
|
||||||
char param[128];
|
|
||||||
uid_t uid = getuid();
|
|
||||||
uint32_t userId = uid / 100000;
|
|
||||||
if (userId == 0) {
|
|
||||||
sprintf(param, "/data/data/%s", pkg);
|
|
||||||
} else {
|
|
||||||
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ksuctl(CMD_BECOME_MANAGER, param, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// cache the result to avoid unnecessary syscall
|
|
||||||
static bool is_lkm;
|
|
||||||
int get_version() {
|
|
||||||
int32_t version = -1;
|
|
||||||
int32_t lkm = 0;
|
|
||||||
ksuctl(CMD_GET_VERSION, &version, &lkm);
|
|
||||||
if (!is_lkm && lkm != 0) {
|
|
||||||
is_lkm = true;
|
|
||||||
}
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get_allow_list(int *uids, int *size) {
|
|
||||||
return ksuctl(CMD_GET_SU_LIST, uids, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_safe_mode() {
|
|
||||||
return ksuctl(CMD_CHECK_SAFEMODE, nullptr, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_lkm_mode() {
|
|
||||||
// you should call get_version first!
|
|
||||||
return is_lkm;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool uid_should_umount(int uid) {
|
|
||||||
bool should;
|
|
||||||
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, reinterpret_cast<void*>(uid), &should) && should;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool set_app_profile(const app_profile *profile) {
|
|
||||||
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get_app_profile(p_key_t key, app_profile *profile) {
|
|
||||||
return ksuctl(CMD_GET_APP_PROFILE, (void*) profile, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool set_su_enabled(bool enabled) {
|
|
||||||
return ksuctl(CMD_ENABLE_SU, (void*) enabled, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_su_enabled() {
|
|
||||||
bool enabled = true;
|
|
||||||
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
|
|
||||||
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
@@ -5,10 +5,14 @@
|
|||||||
#ifndef KERNELSU_KSU_H
|
#ifndef KERNELSU_KSU_H
|
||||||
#define KERNELSU_KSU_H
|
#define KERNELSU_KSU_H
|
||||||
|
|
||||||
|
#include "prelude.h"
|
||||||
#include <linux/capability.h>
|
#include <linux/capability.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
bool become_manager(const char *);
|
bool become_manager(const char *);
|
||||||
|
|
||||||
|
void get_full_version(char* buff);
|
||||||
|
|
||||||
int get_version();
|
int get_version();
|
||||||
|
|
||||||
bool get_allow_list(int *uids, int *size);
|
bool get_allow_list(int *uids, int *size);
|
||||||
@@ -25,7 +29,34 @@ bool is_lkm_mode();
|
|||||||
#define KSU_MAX_GROUPS 32
|
#define KSU_MAX_GROUPS 32
|
||||||
#define KSU_SELINUX_DOMAIN 64
|
#define KSU_SELINUX_DOMAIN 64
|
||||||
|
|
||||||
using p_key_t = char[KSU_MAX_PACKAGE_NAME];
|
#define DYNAMIC_MANAGER_OP_SET 0
|
||||||
|
#define DYNAMIC_MANAGER_OP_GET 1
|
||||||
|
#define DYNAMIC_MANAGER_OP_CLEAR 2
|
||||||
|
|
||||||
|
struct dynamic_manager_user_config {
|
||||||
|
unsigned int operation;
|
||||||
|
unsigned int size;
|
||||||
|
char hash[65];
|
||||||
|
};
|
||||||
|
|
||||||
|
// SUSFS Functional State Structures
|
||||||
|
struct susfs_feature_status {
|
||||||
|
bool status_sus_path;
|
||||||
|
bool status_sus_mount;
|
||||||
|
bool status_auto_default_mount;
|
||||||
|
bool status_auto_bind_mount;
|
||||||
|
bool status_sus_kstat;
|
||||||
|
bool status_try_umount;
|
||||||
|
bool status_auto_try_umount_bind;
|
||||||
|
bool status_spoof_uname;
|
||||||
|
bool status_enable_log;
|
||||||
|
bool status_hide_symbols;
|
||||||
|
bool status_spoof_cmdline;
|
||||||
|
bool status_open_redirect;
|
||||||
|
bool status_magic_mount;
|
||||||
|
bool status_overlayfs_auto_kstat;
|
||||||
|
bool status_sus_su;
|
||||||
|
};
|
||||||
|
|
||||||
struct root_profile {
|
struct root_profile {
|
||||||
int32_t uid;
|
int32_t uid;
|
||||||
@@ -75,12 +106,36 @@ struct app_profile {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
bool set_app_profile(const app_profile *profile);
|
struct manager_list_info {
|
||||||
|
int count;
|
||||||
|
struct {
|
||||||
|
uid_t uid;
|
||||||
|
int signature_index;
|
||||||
|
} managers[2];
|
||||||
|
};
|
||||||
|
|
||||||
bool get_app_profile(p_key_t key, app_profile *profile);
|
bool set_app_profile(const struct app_profile* profile);
|
||||||
|
|
||||||
|
bool get_app_profile(char* key, struct app_profile* profile);
|
||||||
|
|
||||||
bool set_su_enabled(bool enabled);
|
bool set_su_enabled(bool enabled);
|
||||||
|
|
||||||
bool is_su_enabled();
|
bool is_su_enabled();
|
||||||
|
|
||||||
|
bool is_KPM_enable();
|
||||||
|
|
||||||
|
bool get_hook_type(char* hook_type, size_t size);
|
||||||
|
|
||||||
|
bool get_susfs_feature_status(struct susfs_feature_status* status);
|
||||||
|
|
||||||
|
bool set_dynamic_manager(unsigned int size, const char* hash);
|
||||||
|
|
||||||
|
bool get_dynamic_manager(struct dynamic_manager_user_config* config);
|
||||||
|
|
||||||
|
bool clear_dynamic_manager();
|
||||||
|
|
||||||
|
bool get_managers_list(struct manager_list_info* info);
|
||||||
|
|
||||||
|
bool verify_module_signature(const char* input);
|
||||||
|
|
||||||
#endif //KERNELSU_KSU_H
|
#endif //KERNELSU_KSU_H
|
||||||
66
manager/app/src/main/cpp/prelude.h
Normal file
66
manager/app/src/main/cpp/prelude.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
|
||||||
|
#ifndef KERNELSU_PRELUDE_H
|
||||||
|
#define KERNELSU_PRELUDE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <jni.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
#define GetEnvironment() (*env)
|
||||||
|
#define NativeBridge(fn, rtn, ...) JNIEXPORT rtn JNICALL Java_com_sukisu_ultra_Natives_##fn(JNIEnv* env, jclass clazz, __VA_ARGS__)
|
||||||
|
#define NativeBridgeNP(fn, rtn) JNIEXPORT rtn JNICALL Java_com_sukisu_ultra_Natives_##fn(JNIEnv* env, jclass clazz)
|
||||||
|
|
||||||
|
// Macros to simplify field setup
|
||||||
|
#define SET_BOOLEAN_FIELD(obj, cls, fieldName, value) do { \
|
||||||
|
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \
|
||||||
|
GetEnvironment()->SetBooleanField(env, obj, field, value); \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
#define SET_INT_FIELD(obj, cls, fieldName, value) do { \
|
||||||
|
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \
|
||||||
|
GetEnvironment()->SetIntField(env, obj, field, value); \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
#define SET_STRING_FIELD(obj, cls, fieldName, value) do { \
|
||||||
|
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \
|
||||||
|
GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
#define SET_OBJECT_FIELD(obj, cls, fieldName, value) do { \
|
||||||
|
jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \
|
||||||
|
GetEnvironment()->SetObjectField(env, obj, field, value); \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
// Macros for creating Java objects
|
||||||
|
#define CREATE_JAVA_OBJECT(className) ({ \
|
||||||
|
jclass cls = GetEnvironment()->FindClass(env, className); \
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V"); \
|
||||||
|
GetEnvironment()->NewObject(env, cls, constructor); \
|
||||||
|
})
|
||||||
|
|
||||||
|
// Macros for creating ArrayList
|
||||||
|
#define CREATE_ARRAYLIST() ({ \
|
||||||
|
jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "<init>", "()V"); \
|
||||||
|
GetEnvironment()->NewObject(env, arrayListCls, constructor); \
|
||||||
|
})
|
||||||
|
|
||||||
|
// Macros for adding elements to an ArrayList
|
||||||
|
#define ADD_TO_LIST(list, item) do { \
|
||||||
|
jclass cls = GetEnvironment()->GetObjectClass(env, list); \
|
||||||
|
jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \
|
||||||
|
GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \
|
||||||
|
} while(0)
|
||||||
|
|
||||||
|
// Macros for creating Java objects with parameter constructors
|
||||||
|
#define CREATE_JAVA_OBJECT_WITH_PARAMS(className, signature, ...) ({ \
|
||||||
|
jclass cls = GetEnvironment()->FindClass(env, className); \
|
||||||
|
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", signature); \
|
||||||
|
GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define LogDebug(...) __android_log_print(ANDROID_LOG_DEBUG, "KernelSU", __VA_ARGS__)
|
||||||
|
|
||||||
|
#endif
|
||||||
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
BIN
manager/app/src/main/ic_launcher-playstore.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
@@ -1,20 +1,97 @@
|
|||||||
package com.sukisu.ultra
|
package com.sukisu.ultra
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.ActivityOptions
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.content.res.Resources
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
import coil.Coil
|
import coil.Coil
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import com.dergoogler.mmrl.platform.Platform
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
|
||||||
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
lateinit var ksuApp: KernelSUApplication
|
lateinit var ksuApp: KernelSUApplication
|
||||||
|
|
||||||
class KernelSUApplication : Application() {
|
class KernelSUApplication : Application() {
|
||||||
|
private var currentActivity: Activity? = null
|
||||||
|
|
||||||
|
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
|
||||||
|
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||||
|
currentActivity = activity
|
||||||
|
}
|
||||||
|
override fun onActivityStarted(activity: Activity) {
|
||||||
|
currentActivity = activity
|
||||||
|
}
|
||||||
|
override fun onActivityResumed(activity: Activity) {
|
||||||
|
currentActivity = activity
|
||||||
|
}
|
||||||
|
override fun onActivityPaused(activity: Activity) {}
|
||||||
|
override fun onActivityStopped(activity: Activity) {}
|
||||||
|
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
|
||||||
|
override fun onActivityDestroyed(activity: Activity) {
|
||||||
|
if (currentActivity == activity) {
|
||||||
|
currentActivity = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun attachBaseContext(base: Context) {
|
||||||
|
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
var context = base
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val config = Configuration(base.resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
|
||||||
|
context = base.createConfigurationContext(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
override fun getResources(): Resources {
|
||||||
|
val resources = super.getResources()
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
val config = Configuration(resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
return createConfigurationContext(config).resources
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(config, resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resources
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
ksuApp = this
|
ksuApp = this
|
||||||
|
|
||||||
|
// 注册Activity生命周期回调
|
||||||
|
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
|
||||||
|
|
||||||
|
Platform.setHiddenApiExemptions()
|
||||||
|
|
||||||
val context = this
|
val context = this
|
||||||
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
|
||||||
Coil.setImageLoader(
|
Coil.setImageLoader(
|
||||||
@@ -32,5 +109,43 @@ class KernelSUApplication : Application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
applyLanguageSetting()
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ObsoleteSdkInt")
|
||||||
|
private fun applyLanguageSetting() {
|
||||||
|
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
|
val languageCode = prefs.getString("app_language", "") ?: ""
|
||||||
|
|
||||||
|
if (languageCode.isNotEmpty()) {
|
||||||
|
val locale = Locale.forLanguageTag(languageCode)
|
||||||
|
Locale.setDefault(locale)
|
||||||
|
|
||||||
|
val resources = resources
|
||||||
|
val config = Configuration(resources.configuration)
|
||||||
|
config.setLocale(locale)
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
createConfigurationContext(config)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
resources.updateConfiguration(config, resources.displayMetrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加刷新当前Activity的方法
|
||||||
|
fun refreshCurrentActivity() {
|
||||||
|
currentActivity?.let { activity ->
|
||||||
|
val intent = activity.intent
|
||||||
|
activity.finish()
|
||||||
|
|
||||||
|
val options = ActivityOptions.makeCustomAnimation(
|
||||||
|
activity, android.R.anim.fade_in, android.R.anim.fade_out
|
||||||
|
)
|
||||||
|
activity.startActivity(intent, options.toBundle())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,24 +8,13 @@ import android.system.Os
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
|
||||||
override fun toString(): String {
|
override fun toString(): String = "$major.$patchLevel.$subLevel"
|
||||||
return "$major.$patchLevel.$subLevel"
|
fun isGKI(): Boolean = when {
|
||||||
}
|
major > 5 -> true
|
||||||
|
major == 5 && patchLevel >= 10 -> true
|
||||||
fun isGKI(): Boolean {
|
else -> false
|
||||||
|
|
||||||
// kernel 6.x
|
|
||||||
if (major > 5) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// kernel 5.10.x
|
|
||||||
if (major == 5) {
|
|
||||||
return patchLevel >= 10
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
fun isGKI1(): Boolean = (major == 4 && patchLevel >= 19) || (major == 5 && patchLevel < 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun parseKernelVersion(version: String): KernelVersion {
|
fun parseKernelVersion(version: String): KernelVersion {
|
||||||
|
|||||||
@@ -16,20 +16,51 @@ object Natives {
|
|||||||
// 10946: add capabilities
|
// 10946: add capabilities
|
||||||
// 10977: change groups_count and groups to avoid overflow write
|
// 10977: change groups_count and groups to avoid overflow write
|
||||||
// 11071: Fix the issue of failing to set a custom SELinux type.
|
// 11071: Fix the issue of failing to set a custom SELinux type.
|
||||||
const val MINIMAL_SUPPORTED_KERNEL = 12800
|
const val MINIMAL_SUPPORTED_KERNEL = 11071
|
||||||
|
const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.5"
|
||||||
|
|
||||||
// 11640: Support query working mode, LKM or GKI
|
// 11640: Support query working mode, LKM or GKI
|
||||||
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
|
||||||
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800
|
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
|
||||||
|
|
||||||
// 12040: Support disable sucompat mode
|
// 12040: Support disable sucompat mode
|
||||||
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800
|
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
|
||||||
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
|
||||||
|
|
||||||
|
const val MINIMAL_SUPPORTED_KPM = 12800
|
||||||
|
|
||||||
|
const val MINIMAL_SUPPORTED_DYNAMIC_MANAGER = 13215
|
||||||
|
|
||||||
const val ROOT_UID = 0
|
const val ROOT_UID = 0
|
||||||
const val ROOT_GID = 0
|
const val ROOT_GID = 0
|
||||||
|
|
||||||
|
// 获取完整版本号
|
||||||
|
external fun getFullVersion(): String
|
||||||
|
|
||||||
|
fun isVersionLessThan(v1Full: String, v2Full: String): Boolean {
|
||||||
|
fun extractVersionParts(version: String): List<Int> {
|
||||||
|
val match = Regex("""v\d+(\.\d+)*""").find(version)
|
||||||
|
val simpleVersion = match?.value ?: version
|
||||||
|
return simpleVersion.trimStart('v').split('.').map { it.toIntOrNull() ?: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
val v1Parts = extractVersionParts(v1Full)
|
||||||
|
val v2Parts = extractVersionParts(v2Full)
|
||||||
|
val maxLength = maxOf(v1Parts.size, v2Parts.size)
|
||||||
|
for (i in 0 until maxLength) {
|
||||||
|
val num1 = v1Parts.getOrElse(i) { 0 }
|
||||||
|
val num2 = v2Parts.getOrElse(i) { 0 }
|
||||||
|
if (num1 != num2) return num1 < num2
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSimpleVersionFull(): String = getFullVersion().let { version ->
|
||||||
|
Regex("""v\d+(\.\d+)*""").find(version)?.value ?: version
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
System.loadLibrary("zakosign")
|
||||||
System.loadLibrary("zako")
|
System.loadLibrary("zako")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +97,44 @@ object Natives {
|
|||||||
*/
|
*/
|
||||||
external fun isSuEnabled(): Boolean
|
external fun isSuEnabled(): Boolean
|
||||||
external fun setSuEnabled(enabled: Boolean): Boolean
|
external fun setSuEnabled(enabled: Boolean): Boolean
|
||||||
|
external fun isKPMEnabled(): Boolean
|
||||||
|
external fun getHookType(): String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get SUSFS feature status from kernel
|
||||||
|
* @return SusfsFeatureStatus object containing all feature states, or null if failed
|
||||||
|
*/
|
||||||
|
external fun getSusfsFeatureStatus(): SusfsFeatureStatus?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set dynamic managerature configuration
|
||||||
|
* @param size APK signature size
|
||||||
|
* @param hash APK signature hash (64 character hex string)
|
||||||
|
* @return true if successful, false otherwise
|
||||||
|
*/
|
||||||
|
external fun setDynamicManager(size: Int, hash: String): Boolean
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current dynamic managerature configuration
|
||||||
|
* @return DynamicManagerConfig object containing current configuration, or null if not set
|
||||||
|
*/
|
||||||
|
external fun getDynamicManager(): DynamicManagerConfig?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear dynamic managerature configuration
|
||||||
|
* @return true if successful, false otherwise
|
||||||
|
*/
|
||||||
|
external fun clearDynamicManager(): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get active managers list when dynamic manager is enabled
|
||||||
|
* @return ManagersList object containing active managers, or null if failed or not enabled
|
||||||
|
*/
|
||||||
|
external fun getManagersList(): ManagersList?
|
||||||
|
|
||||||
|
// 模块签名验证
|
||||||
|
external fun verifyModuleSignature(modulePath: String): Boolean
|
||||||
|
|
||||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||||
private const val NOBODY_UID = 9999
|
private const val NOBODY_UID = 9999
|
||||||
@@ -88,16 +157,61 @@ object Natives {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun requireNewKernel(): Boolean {
|
fun requireNewKernel(): Boolean {
|
||||||
return version < MINIMAL_SUPPORTED_KERNEL
|
if (version < MINIMAL_SUPPORTED_KERNEL) return true
|
||||||
|
return isVersionLessThan(getFullVersion(), MINIMAL_SUPPORTED_KERNEL_FULL)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isKsuValid(pkgName: String?): Boolean {
|
@Immutable
|
||||||
if (becomeManager(pkgName)) {
|
@Parcelize
|
||||||
return true
|
@Keep
|
||||||
} else {
|
data class SusfsFeatureStatus(
|
||||||
return false
|
val statusSusPath: Boolean = false,
|
||||||
|
val statusSusMount: Boolean = false,
|
||||||
|
val statusAutoDefaultMount: Boolean = false,
|
||||||
|
val statusAutoBindMount: Boolean = false,
|
||||||
|
val statusSusKstat: Boolean = false,
|
||||||
|
val statusTryUmount: Boolean = false,
|
||||||
|
val statusAutoTryUmountBind: Boolean = false,
|
||||||
|
val statusSpoofUname: Boolean = false,
|
||||||
|
val statusEnableLog: Boolean = false,
|
||||||
|
val statusHideSymbols: Boolean = false,
|
||||||
|
val statusSpoofCmdline: Boolean = false,
|
||||||
|
val statusOpenRedirect: Boolean = false,
|
||||||
|
val statusMagicMount: Boolean = false,
|
||||||
|
val statusOverlayfsAutoKstat: Boolean = false,
|
||||||
|
val statusSusSu: Boolean = false
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
|
@Keep
|
||||||
|
data class DynamicManagerConfig(
|
||||||
|
val size: Int = 0,
|
||||||
|
val hash: String = ""
|
||||||
|
) : Parcelable {
|
||||||
|
|
||||||
|
fun isValid(): Boolean {
|
||||||
|
return size > 0 && hash.length == 64 && hash.all {
|
||||||
|
it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
|
@Keep
|
||||||
|
data class ManagersList(
|
||||||
|
val count: Int = 0,
|
||||||
|
val managers: List<ManagerInfo> = emptyList()
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
@Parcelize
|
||||||
|
@Keep
|
||||||
|
data class ManagerInfo(
|
||||||
|
val uid: Int = 0,
|
||||||
|
val signatureIndex: Int = 0
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.sukisu.ultra.ui;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.UserHandle;
|
|
||||||
import android.os.UserManager;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.topjohnwu.superuser.ipc.RootService;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.sukisu.zako.IKsuInterface;
|
|
||||||
import rikka.parcelablelist.ParcelableListSlice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author weishu
|
|
||||||
* @date 2023/4/18.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class KsuService extends RootService {
|
|
||||||
|
|
||||||
private static final String TAG = "KsuService";
|
|
||||||
|
|
||||||
class Stub extends IKsuInterface.Stub {
|
|
||||||
@Override
|
|
||||||
public ParcelableListSlice<PackageInfo> getPackages(int flags) {
|
|
||||||
List<PackageInfo> list = getInstalledPackagesAll(flags);
|
|
||||||
Log.i(TAG, "getPackages: " + list.size());
|
|
||||||
return new ParcelableListSlice<>(list);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(@NonNull Intent intent) {
|
|
||||||
return new Stub();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Integer> getUserIds() {
|
|
||||||
List<Integer> result = new ArrayList<>();
|
|
||||||
UserManager um = (UserManager) getSystemService(Context.USER_SERVICE);
|
|
||||||
List<UserHandle> userProfiles = um.getUserProfiles();
|
|
||||||
for (UserHandle userProfile : userProfiles) {
|
|
||||||
int userId = userProfile.hashCode();
|
|
||||||
result.add(userProfile.hashCode());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayList<PackageInfo> getInstalledPackagesAll(int flags) {
|
|
||||||
ArrayList<PackageInfo> packages = new ArrayList<>();
|
|
||||||
for (Integer userId : getUserIds()) {
|
|
||||||
Log.i(TAG, "getInstalledPackagesAll: " + userId);
|
|
||||||
packages.addAll(getInstalledPackagesAsUser(flags, userId));
|
|
||||||
}
|
|
||||||
return packages;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) {
|
|
||||||
try {
|
|
||||||
PackageManager pm = getPackageManager();
|
|
||||||
Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class);
|
|
||||||
return (List<PackageInfo>) getInstalledPackagesAsUser.invoke(pm, flags, userId);
|
|
||||||
} catch (Throwable e) {
|
|
||||||
Log.e(TAG, "err", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ArrayList<>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
141
manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt
Normal file
141
manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInfo
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.IInterface
|
||||||
|
import android.os.Parcel
|
||||||
|
import android.os.UserManager
|
||||||
|
import android.util.Log
|
||||||
|
import com.topjohnwu.superuser.ipc.RootService
|
||||||
|
import rikka.parcelablelist.ParcelableListSlice
|
||||||
|
import java.lang.reflect.Method
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/7/2.
|
||||||
|
*/
|
||||||
|
class KsuService : RootService() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "KsuService"
|
||||||
|
private const val DESCRIPTOR = "com.sukisu.ultra.IKsuInterface"
|
||||||
|
private const val TRANSACTION_GET_PACKAGES = IBinder.FIRST_CALL_TRANSACTION + 0
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IKsuInterface : IInterface {
|
||||||
|
fun getPackages(flags: Int): ParcelableListSlice<PackageInfo>
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Stub : Binder(), IKsuInterface {
|
||||||
|
init {
|
||||||
|
attachInterface(this, DESCRIPTOR)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun asInterface(obj: IBinder?): IKsuInterface? {
|
||||||
|
if (obj == null) return null
|
||||||
|
val iin = obj.queryLocalInterface(DESCRIPTOR)
|
||||||
|
return if (iin != null && iin is IKsuInterface) {
|
||||||
|
iin
|
||||||
|
} else {
|
||||||
|
Proxy(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asBinder(): IBinder = this
|
||||||
|
|
||||||
|
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
|
||||||
|
val descriptor = DESCRIPTOR
|
||||||
|
when (code) {
|
||||||
|
INTERFACE_TRANSACTION -> {
|
||||||
|
reply?.writeString(descriptor)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
TRANSACTION_GET_PACKAGES -> {
|
||||||
|
data.enforceInterface(descriptor)
|
||||||
|
val flagsArg = data.readInt()
|
||||||
|
val result = getPackages(flagsArg)
|
||||||
|
reply?.writeNoException()
|
||||||
|
reply?.writeInt(1)
|
||||||
|
result.writeToParcel(reply!!, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onTransact(code, data, reply, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Proxy(private val mRemote: IBinder) : IKsuInterface {
|
||||||
|
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
|
||||||
|
val data = Parcel.obtain()
|
||||||
|
val reply = Parcel.obtain()
|
||||||
|
return try {
|
||||||
|
data.writeInterfaceToken(DESCRIPTOR)
|
||||||
|
data.writeInt(flags)
|
||||||
|
mRemote.transact(TRANSACTION_GET_PACKAGES, data, reply, 0)
|
||||||
|
reply.readException()
|
||||||
|
if (reply.readInt() != 0) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
ParcelableListSlice.CREATOR.createFromParcel(reply) as ParcelableListSlice<PackageInfo>
|
||||||
|
} else {
|
||||||
|
ParcelableListSlice(emptyList<PackageInfo>())
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
reply.recycle()
|
||||||
|
data.recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun asBinder(): IBinder = mRemote
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class KsuInterfaceImpl : Stub() {
|
||||||
|
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
|
||||||
|
val list = getInstalledPackagesAll(flags)
|
||||||
|
Log.i(TAG, "getPackages: ${list.size}")
|
||||||
|
return ParcelableListSlice(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent): IBinder {
|
||||||
|
return KsuInterfaceImpl()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserIds(): List<Int> {
|
||||||
|
val result = mutableListOf<Int>()
|
||||||
|
val um = getSystemService(USER_SERVICE) as UserManager
|
||||||
|
val userProfiles = um.userProfiles
|
||||||
|
for (userProfile in userProfiles) {
|
||||||
|
result.add(userProfile.hashCode())
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInstalledPackagesAll(flags: Int): ArrayList<PackageInfo> {
|
||||||
|
val packages = ArrayList<PackageInfo>()
|
||||||
|
for (userId in getUserIds()) {
|
||||||
|
Log.i(TAG, "getInstalledPackagesAll: $userId")
|
||||||
|
packages.addAll(getInstalledPackagesAsUser(flags, userId))
|
||||||
|
}
|
||||||
|
return packages
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List<PackageInfo> {
|
||||||
|
return try {
|
||||||
|
val pm = packageManager
|
||||||
|
val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod(
|
||||||
|
"getInstalledPackagesAsUser",
|
||||||
|
Int::class.java,
|
||||||
|
Int::class.java
|
||||||
|
)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
getInstalledPackagesAsUser.invoke(pm, flags, userId) as List<PackageInfo>
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, "err", e)
|
||||||
|
ArrayList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,168 +1,269 @@
|
|||||||
package com.sukisu.ultra.ui
|
package com.sukisu.ultra.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.*
|
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.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.scaleOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavBackStackEntry
|
import androidx.navigation.NavBackStackEntry
|
||||||
import androidx.navigation.NavHostController
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.ramcosta.composedestinations.DestinationsNavHost
|
import com.ramcosta.composedestinations.DestinationsNavHost
|
||||||
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
|
||||||
import com.ramcosta.composedestinations.generated.NavGraphs
|
import com.ramcosta.composedestinations.generated.NavGraphs
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
|
||||||
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
|
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 io.sukisu.ultra.UltraToolInstall
|
||||||
import com.sukisu.ultra.Natives
|
|
||||||
import com.sukisu.ultra.ksuApp
|
import com.sukisu.ultra.ksuApp
|
||||||
|
import zako.zako.zako.zakoui.activity.util.AppData
|
||||||
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
import com.sukisu.ultra.ui.screen.BottomBarDestination
|
||||||
import com.sukisu.ultra.ui.theme.*
|
import com.sukisu.ultra.ui.theme.*
|
||||||
import com.sukisu.ultra.ui.util.*
|
import zako.zako.zako.zakoui.activity.util.*
|
||||||
|
import zako.zako.zako.zakoui.activity.component.BottomBar
|
||||||
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
|
import com.sukisu.ultra.ui.util.install
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
import com.sukisu.ultra.ui.webui.initPlatform
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var superUserViewModel: SuperUserViewModel
|
||||||
|
private lateinit var homeViewModel: HomeViewModel
|
||||||
|
internal val settingsStateFlow = MutableStateFlow(SettingsState())
|
||||||
|
|
||||||
|
data class SettingsState(
|
||||||
|
val isHideOtherInfo: Boolean = false,
|
||||||
|
val showKpmInfo: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
private lateinit var themeChangeObserver: ThemeChangeContentObserver
|
||||||
|
|
||||||
|
// 添加标记避免重复初始化
|
||||||
|
private var isInitialized = false
|
||||||
|
|
||||||
|
override fun attachBaseContext(newBase: Context) {
|
||||||
|
val context = LocaleUtils.applyLocale(newBase)
|
||||||
|
super.attachBaseContext(context)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
try {
|
||||||
|
// 确保应用正确的语言设置
|
||||||
|
LocaleUtils.applyLanguageSetting(this)
|
||||||
|
|
||||||
|
// 应用自定义 DPI
|
||||||
|
DisplayUtils.applyCustomDpi(this)
|
||||||
|
|
||||||
// Enable edge to edge
|
// Enable edge to edge
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
window.isNavigationBarContrastEnforced = false
|
window.isNavigationBarContrastEnforced = false
|
||||||
}
|
}
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
// 加载保存的背景设置
|
// 使用标记控制初始化流程
|
||||||
loadCustomBackground()
|
if (!isInitialized) {
|
||||||
loadThemeMode()
|
initializeViewModels()
|
||||||
CardConfig.load(applicationContext)
|
initializeData()
|
||||||
|
isInitialized = true
|
||||||
|
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
|
||||||
if (isManager) {
|
|
||||||
install()
|
|
||||||
UltraToolInstall.tryToInstall()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
KernelSUTheme {
|
KernelSUTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val snackBarHostState = remember { SnackbarHostState() }
|
val snackBarHostState = remember { SnackbarHostState() }
|
||||||
|
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
|
||||||
|
|
||||||
|
val bottomBarRoutes = remember {
|
||||||
|
BottomBarDestination.entries.map { it.direction.route }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
val showBottomBar = when (currentDestination?.route) {
|
||||||
|
ExecuteModuleActionScreenDestination.route -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
initPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalSnackbarHost provides snackBarHostState
|
||||||
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = { BottomBar(navController) },
|
bottomBar = {
|
||||||
|
AnimatedBottomBar.AnimatedBottomBarWrapper(
|
||||||
|
showBottomBar = showBottomBar,
|
||||||
|
content = { BottomBar(navController) }
|
||||||
|
)
|
||||||
|
},
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalSnackbarHost provides snackBarHostState,
|
|
||||||
) {
|
|
||||||
DestinationsNavHost(
|
DestinationsNavHost(
|
||||||
modifier = Modifier.padding(innerPadding),
|
modifier = Modifier.padding(innerPadding),
|
||||||
navGraph = NavGraphs.root as NavHostGraphSpec,
|
navGraph = NavGraphs.root as NavHostGraphSpec,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
|
||||||
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
|
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
||||||
get() = { fadeIn(animationSpec = tween(340)) }
|
// If the target is a detail page (not a bottom navigation page), slide in from the right
|
||||||
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
|
if (targetState.destination.route !in bottomBarRoutes) {
|
||||||
get() = { fadeOut(animationSpec = tween(340)) }
|
slideInHorizontally(initialOffsetX = { it })
|
||||||
|
} else {
|
||||||
|
// Otherwise (switching between bottom navigation pages), use fade in
|
||||||
|
fadeIn(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
||||||
|
// If navigating from the home page (bottom navigation page) to a detail page, slide out to the left
|
||||||
|
if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) {
|
||||||
|
slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()
|
||||||
|
} else {
|
||||||
|
// Otherwise (switching between bottom navigation pages), use fade out
|
||||||
|
fadeOut(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
|
||||||
|
// If returning to the home page (bottom navigation page), slide in from the left
|
||||||
|
if (targetState.destination.route in bottomBarRoutes) {
|
||||||
|
slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()
|
||||||
|
} else {
|
||||||
|
// Otherwise (e.g., returning between multiple detail pages), use default fade in
|
||||||
|
fadeIn(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
|
||||||
|
// If returning from a detail page (not a bottom navigation page), scale down and fade out
|
||||||
|
if (initialState.destination.route !in bottomBarRoutes) {
|
||||||
|
scaleOut(targetScale = 0.9f) + fadeOut()
|
||||||
|
} else {
|
||||||
|
// Otherwise, use default fade out
|
||||||
|
fadeOut(animationSpec = tween(340))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
}
|
e.printStackTrace()
|
||||||
|
}
|
||||||
@Composable
|
}
|
||||||
private fun BottomBar(navController: NavHostController) {
|
|
||||||
val navigator = navController.rememberDestinationsNavigator()
|
private fun initializeViewModels() {
|
||||||
val isManager = Natives.becomeManager(ksuApp.packageName)
|
superUserViewModel = SuperUserViewModel()
|
||||||
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
|
homeViewModel = HomeViewModel()
|
||||||
val kpmVersion = getKpmVersion()
|
|
||||||
|
// 设置主题变化监听器
|
||||||
// 获取卡片颜色和透明度
|
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
}
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
|
||||||
val cardElevation = CardConfig.cardElevation
|
private fun initializeData() {
|
||||||
|
lifecycleScope.launch {
|
||||||
NavigationBar(
|
try {
|
||||||
tonalElevation = cardElevation, // 动态设置阴影
|
superUserViewModel.fetchAppList()
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
} catch (e: Exception) {
|
||||||
windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(
|
e.printStackTrace()
|
||||||
WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom
|
}
|
||||||
)
|
}
|
||||||
) {
|
|
||||||
BottomBarDestination.entries.forEach { destination ->
|
lifecycleScope.launch {
|
||||||
if (destination == BottomBarDestination.Kpm) {
|
try {
|
||||||
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) {
|
homeViewModel.initializeData()
|
||||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
} catch (e: Exception) {
|
||||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
e.printStackTrace()
|
||||||
NavigationBarItem(
|
}
|
||||||
selected = isCurrentDestOnBackStack,
|
}
|
||||||
onClick = {
|
|
||||||
if (isCurrentDestOnBackStack) {
|
// 数据刷新协程
|
||||||
navigator.popBackStack(destination.direction, false)
|
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
|
||||||
}
|
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
|
||||||
navigator.navigate(destination.direction) {
|
|
||||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
// 初始化主题相关设置
|
||||||
saveState = true
|
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
|
||||||
}
|
|
||||||
launchSingleTop = true
|
val isManager = AppData.isManager(ksuApp.packageName)
|
||||||
restoreState = true
|
if (isManager) {
|
||||||
}
|
install()
|
||||||
},
|
UltraToolInstall.tryToInstall()
|
||||||
icon = {
|
}
|
||||||
if (isCurrentDestOnBackStack) {
|
}
|
||||||
Icon(destination.iconSelected, stringResource(destination.label))
|
|
||||||
} else {
|
override fun onResume() {
|
||||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
try {
|
||||||
}
|
super.onResume()
|
||||||
},
|
LocaleUtils.applyLanguageSetting(this)
|
||||||
label = { Text(stringResource(destination.label)) },
|
ThemeUtils.onActivityResume()
|
||||||
alwaysShowLabel = false,
|
|
||||||
colors = NavigationBarItemDefaults.colors(
|
// 仅在需要时刷新数据
|
||||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
|
if (isInitialized) {
|
||||||
)
|
refreshData()
|
||||||
)
|
}
|
||||||
}
|
} catch (e: Exception) {
|
||||||
} else {
|
e.printStackTrace()
|
||||||
if (!fullFeatured && destination.rootRequired) return@forEach
|
}
|
||||||
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
|
}
|
||||||
NavigationBarItem(
|
|
||||||
selected = isCurrentDestOnBackStack,
|
private fun refreshData() {
|
||||||
onClick = {
|
lifecycleScope.launch {
|
||||||
if (isCurrentDestOnBackStack) {
|
try {
|
||||||
navigator.popBackStack(destination.direction, false)
|
superUserViewModel.fetchAppList()
|
||||||
}
|
homeViewModel.initializeData()
|
||||||
navigator.navigate(destination.direction) {
|
DataRefreshUtils.refreshData(lifecycleScope)
|
||||||
popUpTo(NavGraphs.root as RouteOrDirection) {
|
} catch (e: Exception) {
|
||||||
saveState = true
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
launchSingleTop = true
|
}
|
||||||
restoreState = true
|
}
|
||||||
}
|
|
||||||
},
|
override fun onPause() {
|
||||||
icon = {
|
try {
|
||||||
if (isCurrentDestOnBackStack) {
|
super.onPause()
|
||||||
Icon(destination.iconSelected, stringResource(destination.label))
|
ThemeUtils.onActivityPause(this)
|
||||||
} else {
|
} catch (e: Exception) {
|
||||||
Icon(destination.iconNotSelected, stringResource(destination.label))
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
label = { Text(stringResource(destination.label)) },
|
|
||||||
alwaysShowLabel = false,
|
override fun onDestroy() {
|
||||||
)
|
try {
|
||||||
}
|
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
|
||||||
|
super.onDestroy()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||||
|
try {
|
||||||
|
super.onConfigurationChanged(newConfig)
|
||||||
|
LocaleUtils.applyLanguageSetting(this)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,10 @@ private fun AboutCardContent() {
|
|||||||
htmlString = stringResource(
|
htmlString = stringResource(
|
||||||
id = R.string.about_source_code,
|
id = R.string.about_source_code,
|
||||||
"<b><a href=\"https://github.com/ShirkNeko/SukiSU-Ultra\">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>",
|
||||||
|
"<b>怡子曰曰</b>",
|
||||||
|
"<b>明风 OuO</b>",
|
||||||
|
"<b><a href=\"https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt\">CC BY-NC-SA 4.0</a></b>"
|
||||||
),
|
),
|
||||||
linkStyles = TextLinkStyles(
|
linkStyles = TextLinkStyles(
|
||||||
style = SpanStyle(
|
style = SpanStyle(
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@SuppressLint("AutoboxingStateCreation")
|
||||||
|
@Composable
|
||||||
|
fun rememberFabVisibilityState(listState: LazyListState): State<Boolean> {
|
||||||
|
var previousScrollOffset by remember { mutableStateOf(0) }
|
||||||
|
var previousIndex by remember { mutableStateOf(0) }
|
||||||
|
val fabVisible = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
LaunchedEffect(listState) {
|
||||||
|
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||||
|
.collect { (index, offset) ->
|
||||||
|
if (previousIndex == 0 && previousScrollOffset == 0) {
|
||||||
|
fabVisible.value = true
|
||||||
|
} else {
|
||||||
|
val isScrollingDown = when {
|
||||||
|
index > previousIndex -> false
|
||||||
|
index < previousIndex -> true
|
||||||
|
else -> offset < previousScrollOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
fabVisible.value = isScrollingDown
|
||||||
|
}
|
||||||
|
|
||||||
|
previousIndex = index
|
||||||
|
previousScrollOffset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fabVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedFab(
|
||||||
|
visible: Boolean,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val scale by animateFloatAsState(
|
||||||
|
targetValue = if (visible) 1f else 0f,
|
||||||
|
animationSpec = spring(
|
||||||
|
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = fadeIn() + scaleIn(),
|
||||||
|
exit = fadeOut() + scaleOut(targetScale = 0.8f)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.scale(scale)
|
||||||
|
.alpha(scale)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
@@ -48,7 +47,6 @@ fun ImageEditorDialog(
|
|||||||
var offsetY by remember { mutableFloatStateOf(0f) }
|
var offsetY by remember { mutableFloatStateOf(0f) }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val density = LocalDensity.current
|
|
||||||
var lastScale by remember { mutableFloatStateOf(1f) }
|
var lastScale by remember { mutableFloatStateOf(1f) }
|
||||||
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
var lastOffsetX by remember { mutableFloatStateOf(0f) }
|
||||||
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
var lastOffsetY by remember { mutableFloatStateOf(0f) }
|
||||||
@@ -139,7 +137,7 @@ fun ImageEditorDialog(
|
|||||||
0f
|
0f
|
||||||
}
|
}
|
||||||
updateTransformation(newScale, newOffsetX, newOffsetY)
|
updateTransformation(newScale, newOffsetX, newOffsetY)
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
updateTransformation(lastScale, lastOffsetX, lastOffsetY)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,7 +185,7 @@ fun ImageEditorDialog(
|
|||||||
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
val transformation = BackgroundTransformation(scale, offsetX, offsetY)
|
||||||
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
val savedUri = context.saveTransformedBackground(imageUri, transformation)
|
||||||
savedUri?.let { onConfirm(it) }
|
savedUri?.let { onConfirm(it) }
|
||||||
} catch (e: Exception) {
|
} catch (_: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,12 @@ fun SearchAppBar(
|
|||||||
var onSearch by remember { mutableStateOf(false) }
|
var onSearch by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
// 获取卡片颜色和透明度
|
// 获取卡片颜色和透明度
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
|
colorScheme.surfaceContainerLow
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
if (onSearch) {
|
if (onSearch) {
|
||||||
|
|||||||
@@ -5,14 +5,20 @@ 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.Color
|
||||||
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
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SwitchItem(
|
fun SwitchItem(
|
||||||
@@ -21,10 +27,17 @@ fun SwitchItem(
|
|||||||
summary: String? = null,
|
summary: String? = null,
|
||||||
checked: Boolean,
|
checked: Boolean,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
|
beta: Boolean = false,
|
||||||
onCheckedChange: (Boolean) -> Unit
|
onCheckedChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.toggleable(
|
.toggleable(
|
||||||
@@ -36,10 +49,30 @@ 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
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
@@ -51,10 +84,14 @@ fun SwitchItem(
|
|||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
if (summary != null) {
|
if (summary != null) {
|
||||||
Text(summary)
|
Text(
|
||||||
|
modifier = Modifier.then(stateAlpha),
|
||||||
|
text = summary
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
package com.sukisu.ultra.ui.component
|
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.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
import androidx.compose.material.icons.Icons
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import androidx.compose.material.icons.filled.SdStorage
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 槽位选择对话框组件
|
* 槽位选择对话框组件
|
||||||
* 用于HorizonKernel刷写时选择目标槽位
|
* 用于Kernel刷写时选择目标槽位
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -28,13 +29,19 @@ fun SlotSelectionDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onSlotSelected: (String) -> Unit
|
onSlotSelected: (String) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
var currentSlot by remember { mutableStateOf<String?>(null) }
|
var currentSlot by remember { mutableStateOf<String?>(null) }
|
||||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
|
var selectedSlot by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
try {
|
try {
|
||||||
currentSlot = getCurrentSlot(context)
|
currentSlot = getCurrentSlot()
|
||||||
|
// 设置默认选择为当前槽位
|
||||||
|
selectedSlot = when (currentSlot) {
|
||||||
|
"a" -> "a"
|
||||||
|
"b" -> "b"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
errorMessage = null
|
errorMessage = null
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
errorMessage = e.message
|
errorMessage = e.message
|
||||||
@@ -43,38 +50,22 @@ fun SlotSelectionDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (show) {
|
if (show) {
|
||||||
val backgroundColor = if (!ThemeConfig.useDynamicColor) {
|
val cardColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
ThemeConfig.currentTheme.ButtonContrast.copy(alpha = 1.0f)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 1.0f)
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
AlertDialog(
|
||||||
Card(
|
onDismissRequest = onDismiss,
|
||||||
shape = MaterialTheme.shapes.medium.copy(
|
title = {
|
||||||
topStart = CornerSize(16.dp),
|
|
||||||
topEnd = CornerSize(16.dp),
|
|
||||||
bottomEnd = CornerSize(16.dp),
|
|
||||||
bottomStart = CornerSize(16.dp)
|
|
||||||
),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = backgroundColor
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(24.dp)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = R.string.select_slot_title),
|
text = stringResource(id = R.string.select_slot_title),
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
textAlign = TextAlign.Center,
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
modifier = Modifier.padding(bottom = 16.dp)
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
if (errorMessage != null) {
|
if (errorMessage != null) {
|
||||||
Text(
|
Text(
|
||||||
text = "Error: $errorMessage",
|
text = "Error: $errorMessage",
|
||||||
@@ -105,89 +96,146 @@ fun SlotSelectionDialog(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
// Horizontal arrangement for slot options with highlighted current slot
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
.fillMaxWidth()
|
||||||
) {
|
.horizontalScroll(rememberScrollState()),
|
||||||
val isDefaultSlotA = currentSlot == "_a" || currentSlot == "a"
|
|
||||||
Button(
|
|
||||||
onClick = { onSlotSelected("a") },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = if (isDefaultSlotA)
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
contentColor = if (isDefaultSlotA)
|
|
||||||
MaterialTheme.colorScheme.onPrimary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.slot_a),
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val isDefaultSlotB = currentSlot == "_b" || currentSlot == "b"
|
|
||||||
Button(
|
|
||||||
onClick = { onSlotSelected("b") },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
colors = ButtonDefaults.buttonColors(
|
|
||||||
containerColor = if (isDefaultSlotB)
|
|
||||||
MaterialTheme.colorScheme.secondary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
contentColor = if (isDefaultSlotB)
|
|
||||||
MaterialTheme.colorScheme.onSecondary
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.slot_b),
|
|
||||||
style = MaterialTheme.typography.labelLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
thickness = 1.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
) {
|
) {
|
||||||
TextButton(
|
val slotOptions = listOf(
|
||||||
onClick = onDismiss,
|
ListOption(
|
||||||
|
titleText = stringResource(id = R.string.slot_a),
|
||||||
|
subtitleText = null,
|
||||||
|
icon = Icons.Filled.SdStorage
|
||||||
|
),
|
||||||
|
ListOption(
|
||||||
|
titleText = stringResource(id = R.string.slot_b),
|
||||||
|
subtitleText = 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 (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
|
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
selectedSlot = when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp, horizontal = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = option.icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = android.R.string.cancel))
|
Text(
|
||||||
|
text = option.titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
color = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
option.subtitleText?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (selectedSlot == when(index) {
|
||||||
|
0 -> "a"
|
||||||
|
else -> "b"
|
||||||
|
}) {
|
||||||
|
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
currentSlot?.let { onSlotSelected(it) }
|
selectedSlot?.let { onSlotSelected(it) }
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
modifier = Modifier.weight(1f)
|
enabled = selectedSlot != null
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(id = android.R.string.ok))
|
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
|
||||||
private fun getCurrentSlot(context: Context): String? {
|
data class ListOption(
|
||||||
|
val titleText: String,
|
||||||
|
val subtitleText: String?,
|
||||||
|
val icon: ImageVector
|
||||||
|
)
|
||||||
|
|
||||||
|
// Utility function to get current slot
|
||||||
|
private fun getCurrentSlot(): String? {
|
||||||
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
|
||||||
if (it.startsWith("_")) it.substring(1) else it
|
if (it.startsWith("_")) it.substring(1) else it
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,861 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Apps
|
||||||
|
import androidx.compose.material.icons.filled.Folder
|
||||||
|
import androidx.compose.material.icons.filled.Loop
|
||||||
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
|
import androidx.compose.material.icons.filled.Security
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Storage
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.util.SuSFSManager
|
||||||
|
import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SUS路径内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SusPathsContent(
|
||||||
|
susPaths: Set<String>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddPath: () -> Unit,
|
||||||
|
onAddAppPath: () -> Unit,
|
||||||
|
onRemovePath: (String) -> Unit,
|
||||||
|
onEditPath: ((String) -> Unit)? = null,
|
||||||
|
forceRefreshApps: Boolean = false
|
||||||
|
) {
|
||||||
|
val superUserApps = SuperUserViewModel.apps
|
||||||
|
val superUserIsRefreshing = remember { SuperUserViewModel().isRefreshing }
|
||||||
|
|
||||||
|
LaunchedEffect(superUserIsRefreshing, superUserApps.size) {
|
||||||
|
if (!superUserIsRefreshing && superUserApps.isNotEmpty()) {
|
||||||
|
AppInfoCache.clearCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(forceRefreshApps) {
|
||||||
|
if (forceRefreshApps) {
|
||||||
|
AppInfoCache.clearCache()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (appPathGroups, otherPaths) = remember(susPaths) {
|
||||||
|
val appPathRegex = Regex(".*/Android/data/([^/]+)/?.*")
|
||||||
|
val uidPathRegex = Regex("/sys/fs/cgroup/uid_([0-9]+)")
|
||||||
|
val appPathMap = mutableMapOf<String, MutableList<String>>()
|
||||||
|
val uidToPackageMap = mutableMapOf<String, String>()
|
||||||
|
val others = mutableListOf<String>()
|
||||||
|
|
||||||
|
// 构建UID到包名的映射
|
||||||
|
SuperUserViewModel.apps.forEach { app ->
|
||||||
|
try {
|
||||||
|
val uid = app.packageInfo.applicationInfo?.uid
|
||||||
|
uidToPackageMap[uid.toString()] = app.packageName
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
susPaths.forEach { path ->
|
||||||
|
val appDataMatch = appPathRegex.find(path)
|
||||||
|
val uidMatch = uidPathRegex.find(path)
|
||||||
|
|
||||||
|
when {
|
||||||
|
appDataMatch != null -> {
|
||||||
|
val packageName = appDataMatch.groupValues[1]
|
||||||
|
appPathMap.getOrPut(packageName) { mutableListOf() }.add(path)
|
||||||
|
}
|
||||||
|
uidMatch != null -> {
|
||||||
|
val uid = uidMatch.groupValues[1]
|
||||||
|
val packageName = uidToPackageMap[uid]
|
||||||
|
if (packageName != null) {
|
||||||
|
appPathMap.getOrPut(packageName) { mutableListOf() }.add(path)
|
||||||
|
} else {
|
||||||
|
others.add(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
others.add(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortedAppGroups = appPathMap.toList()
|
||||||
|
.sortedBy { it.first }
|
||||||
|
.map { (packageName, paths) -> packageName to paths.sorted() }
|
||||||
|
|
||||||
|
Pair(sortedAppGroups, others.sorted())
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// 应用路径分组
|
||||||
|
if (appPathGroups.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.app_paths_section),
|
||||||
|
subtitle = null,
|
||||||
|
icon = Icons.Default.Apps,
|
||||||
|
count = appPathGroups.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(appPathGroups) { (packageName, paths) ->
|
||||||
|
AppPathGroupCard(
|
||||||
|
packageName = packageName,
|
||||||
|
paths = paths,
|
||||||
|
onDeleteGroup = {
|
||||||
|
paths.forEach { path -> onRemovePath(path) }
|
||||||
|
},
|
||||||
|
onEditGroup = if (onEditPath != null) {
|
||||||
|
{
|
||||||
|
onEditPath(paths.first())
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他路径
|
||||||
|
if (otherPaths.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.other_paths_section),
|
||||||
|
subtitle = null,
|
||||||
|
icon = Icons.Default.Folder,
|
||||||
|
count = otherPaths.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(otherPaths) { path ->
|
||||||
|
PathItemCard(
|
||||||
|
path = path,
|
||||||
|
icon = Icons.Default.Folder,
|
||||||
|
onDelete = { onRemovePath(path) },
|
||||||
|
onEdit = if (onEditPath != null) { { onEditPath(path) } } else null,
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (susPaths.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_paths_configured)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onAddPath,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add_custom_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onAddAppPath,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Apps,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add_app_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SUS循环路径内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SusLoopPathsContent(
|
||||||
|
susLoopPaths: Set<String>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddLoopPath: () -> Unit,
|
||||||
|
onRemoveLoopPath: (String) -> Unit,
|
||||||
|
onEditLoopPath: ((String) -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
// 说明卡片
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sus_loop_paths_description_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.sus_loop_paths_description_text),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_loop_path_restriction_warning),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.secondary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (susLoopPaths.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_loop_paths_configured)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
SectionHeader(
|
||||||
|
title = stringResource(R.string.loop_paths_section),
|
||||||
|
subtitle = null,
|
||||||
|
icon = Icons.Default.Loop,
|
||||||
|
count = susLoopPaths.size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(susLoopPaths.toList()) { path ->
|
||||||
|
PathItemCard(
|
||||||
|
path = path,
|
||||||
|
icon = Icons.Default.Loop,
|
||||||
|
onDelete = { onRemoveLoopPath(path) },
|
||||||
|
onEdit = if (onEditLoopPath != null) { { onEditLoopPath(path) } } else null,
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onAddLoopPath,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add_loop_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SUS挂载内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SusMountsContent(
|
||||||
|
susMounts: Set<String>,
|
||||||
|
hideSusMountsForAllProcs: Boolean,
|
||||||
|
isSusVersion158: Boolean,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddMount: () -> Unit,
|
||||||
|
onRemoveMount: (String) -> Unit,
|
||||||
|
onEditMount: ((String) -> Unit)? = null,
|
||||||
|
onToggleHideSusMountsForAllProcs: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
if (isSusVersion158) {
|
||||||
|
item {
|
||||||
|
SusMountHidingControlCard(
|
||||||
|
hideSusMountsForAllProcs = hideSusMountsForAllProcs,
|
||||||
|
isLoading = isLoading,
|
||||||
|
onToggleHiding = onToggleHideSusMountsForAllProcs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (susMounts.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_mounts_configured)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(susMounts.toList()) { mount ->
|
||||||
|
PathItemCard(
|
||||||
|
path = mount,
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
onDelete = { onRemoveMount(mount) },
|
||||||
|
onEdit = if (onEditMount != null) { { onEditMount(mount) } } else null,
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onAddMount,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 尝试卸载内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TryUmountContent(
|
||||||
|
tryUmounts: Set<String>,
|
||||||
|
umountForZygoteIsoService: Boolean,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddUmount: () -> Unit,
|
||||||
|
onRunUmount: () -> Unit,
|
||||||
|
onRemoveUmount: (String) -> Unit,
|
||||||
|
onEditUmount: ((String) -> Unit)? = null,
|
||||||
|
onToggleUmountForZygoteIsoService: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
if (isSusVersion158()) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Security,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.umount_zygote_iso_service),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(6.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.umount_zygote_iso_service_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
lineHeight = 14.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = umountForZygoteIsoService,
|
||||||
|
onCheckedChange = onToggleUmountForZygoteIsoService,
|
||||||
|
enabled = !isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tryUmounts.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_umounts_configured)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(tryUmounts.toList()) { umountEntry ->
|
||||||
|
val parts = umountEntry.split("|")
|
||||||
|
val path = if (parts.isNotEmpty()) parts[0] else umountEntry
|
||||||
|
val mode = if (parts.size > 1) parts[1] else "0"
|
||||||
|
val modeText = if (mode == "0")
|
||||||
|
stringResource(R.string.susfs_umount_mode_normal_short)
|
||||||
|
else
|
||||||
|
stringResource(R.string.susfs_umount_mode_detach_short)
|
||||||
|
|
||||||
|
PathItemCard(
|
||||||
|
path = path,
|
||||||
|
icon = Icons.Default.Storage,
|
||||||
|
additionalInfo = stringResource(R.string.susfs_umount_mode_display, modeText, mode),
|
||||||
|
onDelete = { onRemoveUmount(umountEntry) },
|
||||||
|
onEdit = if (onEditUmount != null) { { onEditUmount(umountEntry) } } else null,
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onAddUmount,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tryUmounts.isNotEmpty()) {
|
||||||
|
Button(
|
||||||
|
onClick = onRunUmount,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.PlayArrow,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.susfs_run))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kstat配置内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun KstatConfigContent(
|
||||||
|
kstatConfigs: Set<String>,
|
||||||
|
addKstatPaths: Set<String>,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onAddKstatStatically: () -> Unit,
|
||||||
|
onAddKstat: () -> Unit,
|
||||||
|
onRemoveKstatConfig: (String) -> Unit,
|
||||||
|
onEditKstatConfig: ((String) -> Unit)? = null,
|
||||||
|
onRemoveAddKstat: (String) -> Unit,
|
||||||
|
onEditAddKstat: ((String) -> Unit)? = null,
|
||||||
|
onUpdateKstat: (String) -> Unit,
|
||||||
|
onUpdateKstatFullClone: (String) -> Unit
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kstat_config_description_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kstat_config_description_add_statically),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kstat_config_description_add),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kstat_config_description_update),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kstat_config_description_update_full_clone),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kstatConfigs.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.static_kstat_config),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(kstatConfigs.toList()) { config ->
|
||||||
|
KstatConfigItemCard(
|
||||||
|
config = config,
|
||||||
|
onDelete = { onRemoveKstatConfig(config) },
|
||||||
|
onEdit = if (onEditKstatConfig != null) { { onEditKstatConfig(config) } } else null,
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (addKstatPaths.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kstat_path_management),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(addKstatPaths.toList()) { path ->
|
||||||
|
AddKstatPathItemCard(
|
||||||
|
path = path,
|
||||||
|
onDelete = { onRemoveAddKstat(path) },
|
||||||
|
onEdit = if (onEditAddKstat != null) { { onEditAddKstat(path) } } else null,
|
||||||
|
onUpdate = { onUpdateKstat(path) },
|
||||||
|
onUpdateFullClone = { onUpdateKstatFullClone(path) },
|
||||||
|
isLoading = isLoading
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kstatConfigs.isEmpty() && addKstatPaths.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.no_kstat_config_message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
onClick = onAddKstat,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onAddKstatStatically,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = stringResource(R.string.add))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路径设置内容组件
|
||||||
|
*/
|
||||||
|
@SuppressLint("SdCardPath")
|
||||||
|
@Composable
|
||||||
|
fun PathSettingsContent(
|
||||||
|
androidDataPath: String,
|
||||||
|
onAndroidDataPathChange: (String) -> Unit,
|
||||||
|
sdcardPath: String,
|
||||||
|
onSdcardPathChange: (String) -> Unit,
|
||||||
|
isLoading: Boolean,
|
||||||
|
onSetAndroidDataPath: () -> Unit,
|
||||||
|
onSetSdcardPath: () -> Unit
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = androidDataPath,
|
||||||
|
onValueChange = onAndroidDataPathChange,
|
||||||
|
label = { Text(stringResource(R.string.susfs_android_data_path_label)) },
|
||||||
|
placeholder = { Text("/sdcard/Android/data") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isLoading,
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onSetAndroidDataPath,
|
||||||
|
enabled = !isLoading && androidDataPath.isNotBlank(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(40.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.susfs_set_android_data_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = sdcardPath,
|
||||||
|
onValueChange = onSdcardPathChange,
|
||||||
|
label = { Text(stringResource(R.string.susfs_sdcard_path_label)) },
|
||||||
|
placeholder = { Text("/sdcard") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = !isLoading,
|
||||||
|
singleLine = true,
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = onSetSdcardPath,
|
||||||
|
enabled = !isLoading && sdcardPath.isNotBlank(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(40.dp),
|
||||||
|
shape = RoundedCornerShape(8.dp)
|
||||||
|
) {
|
||||||
|
Text(stringResource(R.string.susfs_set_sdcard_path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用功能状态内容组件
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun EnabledFeaturesContent(
|
||||||
|
enabledFeatures: List<SuSFSManager.EnabledFeature>,
|
||||||
|
onRefresh: () -> Unit
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.susfs_enabled_features_description),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabledFeatures.isEmpty()) {
|
||||||
|
item {
|
||||||
|
EmptyStateCard(
|
||||||
|
message = stringResource(R.string.susfs_no_features_found)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items(enabledFeatures) { feature ->
|
||||||
|
FeatureStatusCard(
|
||||||
|
feature = feature,
|
||||||
|
onRefresh = onRefresh
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.sukisu.ultra.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package com.sukisu.ultra.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.*
|
||||||
|
import androidx.compose.animation.core.*
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import com.sukisu.ultra.R
|
||||||
|
|
||||||
|
// 菜单项数据类
|
||||||
|
data class FabMenuItem(
|
||||||
|
val icon: ImageVector,
|
||||||
|
val labelRes: Int,
|
||||||
|
val color: Color = Color.Unspecified,
|
||||||
|
val onClick: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
// 动画配置
|
||||||
|
object FabAnimationConfig {
|
||||||
|
const val ANIMATION_DURATION = 300
|
||||||
|
const val STAGGER_DELAY = 50
|
||||||
|
val BUTTON_SPACING = 72.dp
|
||||||
|
val BUTTON_SIZE = 56.dp
|
||||||
|
val SMALL_BUTTON_SIZE = 48.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalExpandableFab(
|
||||||
|
menuItems: List<FabMenuItem>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
buttonSize: Dp = FabAnimationConfig.BUTTON_SIZE,
|
||||||
|
smallButtonSize: Dp = FabAnimationConfig.SMALL_BUTTON_SIZE,
|
||||||
|
buttonSpacing: Dp = FabAnimationConfig.BUTTON_SPACING,
|
||||||
|
animationDurationMs: Int = FabAnimationConfig.ANIMATION_DURATION,
|
||||||
|
staggerDelayMs: Int = FabAnimationConfig.STAGGER_DELAY,
|
||||||
|
mainButtonIcon: ImageVector = Icons.Filled.Add,
|
||||||
|
mainButtonExpandedIcon: ImageVector = Icons.Filled.Close,
|
||||||
|
onMainButtonClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// 主按钮旋转动画
|
||||||
|
val rotationAngle by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) 45f else 0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = animationDurationMs,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "mainButtonRotation"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 主按钮缩放动画
|
||||||
|
val mainButtonScale by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) 1.1f else 1f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = animationDurationMs,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "mainButtonScale"
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier.wrapContentSize(),
|
||||||
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
) {
|
||||||
|
// 子菜单按钮
|
||||||
|
menuItems.forEachIndexed { index, menuItem ->
|
||||||
|
val animatedOffsetY by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) {
|
||||||
|
-(buttonSpacing.value * (index + 1))
|
||||||
|
} else {
|
||||||
|
0f
|
||||||
|
},
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = animationDurationMs,
|
||||||
|
delayMillis = if (isExpanded) {
|
||||||
|
index * staggerDelayMs
|
||||||
|
} else {
|
||||||
|
(menuItems.size - index - 1) * staggerDelayMs
|
||||||
|
},
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "fabOffset$index"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedScale by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) 1f else 0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = animationDurationMs,
|
||||||
|
delayMillis = if (isExpanded) {
|
||||||
|
index * staggerDelayMs + 100
|
||||||
|
} else {
|
||||||
|
(menuItems.size - index - 1) * staggerDelayMs
|
||||||
|
},
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "fabScale$index"
|
||||||
|
)
|
||||||
|
|
||||||
|
val animatedAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (isExpanded) 1f else 0f,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = animationDurationMs,
|
||||||
|
delayMillis = if (isExpanded) {
|
||||||
|
index * staggerDelayMs + 150
|
||||||
|
} else {
|
||||||
|
(menuItems.size - index - 1) * staggerDelayMs
|
||||||
|
},
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "fabAlpha$index"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 子按钮容器(包含标签)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(y = animatedOffsetY.dp)
|
||||||
|
.scale(animatedScale)
|
||||||
|
.alpha(animatedAlpha),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
// 标签
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isExpanded && animatedScale > 0.5f,
|
||||||
|
enter = slideInHorizontally(
|
||||||
|
initialOffsetX = { it / 2 },
|
||||||
|
animationSpec = tween(200)
|
||||||
|
) + fadeIn(animationSpec = tween(200)),
|
||||||
|
exit = slideOutHorizontally(
|
||||||
|
targetOffsetX = { it / 2 },
|
||||||
|
animationSpec = tween(150)
|
||||||
|
) + fadeOut(animationSpec = tween(150))
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(end = 16.dp),
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
color = MaterialTheme.colorScheme.inverseSurface,
|
||||||
|
tonalElevation = 6.dp
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(menuItem.labelRes),
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.inverseOnSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子按钮
|
||||||
|
SmallFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
menuItem.onClick()
|
||||||
|
isExpanded = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(smallButtonSize),
|
||||||
|
containerColor = if (menuItem.color != Color.Unspecified) {
|
||||||
|
menuItem.color
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.secondary
|
||||||
|
},
|
||||||
|
contentColor = if (menuItem.color != Color.Unspecified) {
|
||||||
|
if (menuItem.color == Color.Gray) Color.White
|
||||||
|
else MaterialTheme.colorScheme.onSecondary
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSecondary
|
||||||
|
},
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(
|
||||||
|
defaultElevation = 4.dp,
|
||||||
|
pressedElevation = 6.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = menuItem.icon,
|
||||||
|
contentDescription = stringResource(menuItem.labelRes),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主按钮
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
onMainButtonClick?.invoke()
|
||||||
|
isExpanded = !isExpanded
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.size(buttonSize)
|
||||||
|
.scale(mainButtonScale),
|
||||||
|
elevation = FloatingActionButtonDefaults.elevation(
|
||||||
|
defaultElevation = 6.dp,
|
||||||
|
pressedElevation = 8.dp,
|
||||||
|
hoveredElevation = 8.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isExpanded) mainButtonExpandedIcon else mainButtonIcon,
|
||||||
|
contentDescription = stringResource(
|
||||||
|
if (isExpanded) R.string.collapse_menu else R.string.expand_menu
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.rotate(if (mainButtonIcon == Icons.Filled.Add) rotationAngle else 0f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预设菜单项
|
||||||
|
object FabMenuPresets {
|
||||||
|
fun getScrollMenuItems(
|
||||||
|
onScrollToTop: () -> Unit,
|
||||||
|
onScrollToBottom: () -> Unit
|
||||||
|
) = listOf(
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.KeyboardArrowDown,
|
||||||
|
labelRes = R.string.scroll_to_bottom,
|
||||||
|
onClick = onScrollToBottom
|
||||||
|
),
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.KeyboardArrowUp,
|
||||||
|
labelRes = R.string.scroll_to_top,
|
||||||
|
onClick = onScrollToTop
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun getBatchActionMenuItems(
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onDeny: () -> Unit,
|
||||||
|
onAllow: () -> Unit,
|
||||||
|
onUnmountModules: () -> Unit,
|
||||||
|
onDisableUnmount: () -> Unit
|
||||||
|
) = listOf(
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.Close,
|
||||||
|
labelRes = R.string.cancel,
|
||||||
|
color = Color.Gray,
|
||||||
|
onClick = onCancel
|
||||||
|
),
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.Block,
|
||||||
|
labelRes = R.string.deny_authorization,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
onClick = onDeny
|
||||||
|
),
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.Check,
|
||||||
|
labelRes = R.string.grant_authorization,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
onClick = onAllow
|
||||||
|
),
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.FolderOff,
|
||||||
|
labelRes = R.string.unmount_modules,
|
||||||
|
onClick = onUnmountModules
|
||||||
|
),
|
||||||
|
FabMenuItem(
|
||||||
|
icon = Icons.Filled.Folder,
|
||||||
|
labelRes = R.string.disable_unmount,
|
||||||
|
onClick = onDisableUnmount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -31,7 +31,6 @@ fun AppProfileConfig(
|
|||||||
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
onValueChange = { onProfileChange(profile.copy(name = it)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
title = stringResource(R.string.profile_umount_modules),
|
title = stringResource(R.string.profile_umount_modules),
|
||||||
summary = stringResource(R.string.profile_umount_modules_summary),
|
summary = stringResource(R.string.profile_umount_modules_summary),
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
@@ -47,6 +48,8 @@ import com.sukisu.ultra.R
|
|||||||
import com.sukisu.ultra.profile.Capabilities
|
import com.sukisu.ultra.profile.Capabilities
|
||||||
import com.sukisu.ultra.profile.Groups
|
import com.sukisu.ultra.profile.Groups
|
||||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.util.isSepolicyValid
|
import com.sukisu.ultra.ui.util.isSepolicyValid
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -206,6 +209,12 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selection = HashSet(selected)
|
val selection = HashSet(selected)
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
ListDialog(
|
ListDialog(
|
||||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
closeSelection(selection)
|
closeSelection(selection)
|
||||||
@@ -229,6 +238,7 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -278,6 +288,12 @@ fun CapsPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val selection = HashSet(selected)
|
val selection = HashSet(selected)
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
ListDialog(
|
ListDialog(
|
||||||
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
closeSelection(selection)
|
closeSelection(selection)
|
||||||
@@ -300,6 +316,7 @@ fun CapsPanel(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -425,8 +442,15 @@ private fun SELinuxPanel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
InputDialog(
|
InputDialog(
|
||||||
state = rememberUseCaseState(visible = true,
|
state = rememberUseCaseState(
|
||||||
|
visible = true,
|
||||||
onFinishedRequest = {
|
onFinishedRequest = {
|
||||||
onSELinuxChange(domain, rules)
|
onSELinuxChange(domain, rules)
|
||||||
},
|
},
|
||||||
@@ -444,6 +468,7 @@ private fun SELinuxPanel(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ListItem(headlineContent = {
|
ListItem(headlineContent = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
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.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -25,16 +31,20 @@ import androidx.compose.material.icons.filled.Android
|
|||||||
import androidx.compose.material.icons.filled.Security
|
import androidx.compose.material.icons.filled.Security
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
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.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -44,8 +54,12 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -63,13 +77,16 @@ import com.ramcosta.composedestinations.annotation.RootGraph
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.component.SwitchItem
|
import com.sukisu.ultra.ui.component.SwitchItem
|
||||||
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
import com.sukisu.ultra.ui.component.profile.AppProfileConfig
|
||||||
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
import com.sukisu.ultra.ui.component.profile.RootProfileConfig
|
||||||
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
import com.sukisu.ultra.ui.component.profile.TemplateConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardColors
|
||||||
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.util.forceStopApp
|
import com.sukisu.ultra.ui.util.forceStopApp
|
||||||
import com.sukisu.ultra.ui.util.getSepolicy
|
import com.sukisu.ultra.ui.util.getSepolicy
|
||||||
@@ -78,6 +95,7 @@ import com.sukisu.ultra.ui.util.restartApp
|
|||||||
import com.sukisu.ultra.ui.util.setSepolicy
|
import com.sukisu.ultra.ui.util.setSepolicy
|
||||||
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
|
||||||
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author weishu
|
||||||
@@ -107,9 +125,23 @@ fun AppProfileScreen(
|
|||||||
mutableStateOf(initialProfile)
|
mutableStateOf(initialProfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
|
colorScheme.surfaceContainerLow
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
|
title = appInfo.label,
|
||||||
|
packageName = packageName,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
onBack = dropUnlessResumed { navigator.popBackStack() },
|
onBack = dropUnlessResumed { navigator.popBackStack() },
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
@@ -179,24 +211,62 @@ private fun AppProfileInner(
|
|||||||
onProfileChange: (Natives.Profile) -> Unit,
|
onProfileChange: (Natives.Profile) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isRootGranted = profile.allowSu
|
val isRootGranted = profile.allowSu
|
||||||
|
val cardColors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
) {
|
||||||
AppMenuBox(packageName) {
|
AppMenuBox(packageName) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(appLabel) },
|
headlineContent = {
|
||||||
supportingContent = { Text(packageName) },
|
Text(
|
||||||
|
text = appLabel,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = packageName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
leadingContent = appIcon,
|
leadingContent = appIcon,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
) {
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.Security,
|
icon = Icons.Filled.Security,
|
||||||
title = stringResource(id = R.string.superuser),
|
title = stringResource(id = R.string.superuser),
|
||||||
checked = isRootGranted,
|
checked = isRootGranted,
|
||||||
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Crossfade(targetState = isRootGranted, label = "") { current ->
|
Crossfade(
|
||||||
|
targetState = isRootGranted,
|
||||||
|
label = "RootAccess"
|
||||||
|
) { current ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
|
||||||
) {
|
) {
|
||||||
@@ -211,6 +281,15 @@ private fun AppProfileInner(
|
|||||||
var mode by rememberSaveable {
|
var mode by rememberSaveable {
|
||||||
mutableStateOf(initialMode)
|
mutableStateOf(initialMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
) {
|
||||||
ProfileBox(mode, true) {
|
ProfileBox(mode, true) {
|
||||||
// template mode shouldn't change profile here!
|
// template mode shouldn't change profile here!
|
||||||
if (it == Mode.Default || it == Mode.Custom) {
|
if (it == Mode.Default || it == Mode.Custom) {
|
||||||
@@ -218,33 +297,84 @@ private fun AppProfileInner(
|
|||||||
}
|
}
|
||||||
mode = it
|
mode = it
|
||||||
}
|
}
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
}
|
||||||
if (currentMode == Mode.Template) {
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = mode != Mode.Default,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
|
Crossfade(
|
||||||
|
targetState = mode,
|
||||||
|
label = "ProfileMode"
|
||||||
|
) { currentMode ->
|
||||||
|
when (currentMode) {
|
||||||
|
Mode.Template -> {
|
||||||
TemplateConfig(
|
TemplateConfig(
|
||||||
profile = profile,
|
profile = profile,
|
||||||
onViewTemplate = onViewTemplate,
|
onViewTemplate = onViewTemplate,
|
||||||
onManageTemplate = onManageTemplate,
|
onManageTemplate = onManageTemplate,
|
||||||
onProfileChange = onProfileChange
|
onProfileChange = onProfileChange
|
||||||
)
|
)
|
||||||
} else if (mode == Mode.Custom) {
|
}
|
||||||
|
|
||||||
|
Mode.Custom -> {
|
||||||
RootProfileConfig(
|
RootProfileConfig(
|
||||||
fixedName = true,
|
fixedName = true,
|
||||||
profile = profile,
|
profile = profile,
|
||||||
onProfileChange = onProfileChange
|
onProfileChange = onProfileChange
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom
|
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,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
) {
|
||||||
ProfileBox(mode, false) {
|
ProfileBox(mode, false) {
|
||||||
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
|
||||||
}
|
}
|
||||||
Crossfade(targetState = mode, label = "") { currentMode ->
|
}
|
||||||
val modifyEnabled = currentMode == Mode.Custom
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = mode == Mode.Custom,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = cardColors,
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
AppProfileConfig(
|
AppProfileConfig(
|
||||||
fixedName = true,
|
fixedName = true,
|
||||||
profile = profile,
|
profile = profile,
|
||||||
enabled = modifyEnabled,
|
enabled = mode == Mode.Custom,
|
||||||
onProfileChange = onProfileChange
|
onProfileChange = onProfileChange
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -252,6 +382,9 @@ private fun AppProfileInner(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class Mode(@StringRes private val res: Int) {
|
private enum class Mode(@StringRes private val res: Int) {
|
||||||
@@ -264,20 +397,45 @@ private enum class Mode(@StringRes private val res: Int) {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
|
title: String,
|
||||||
|
packageName: String,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(stringResource(R.string.profile))
|
Column {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = packageName,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.alpha(0.8f)
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
colors = colors,
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onBack
|
onClick = onBack,
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.back)
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
scrollBehavior = scrollBehavior
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
modifier = Modifier.shadow(
|
||||||
|
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
|
||||||
|
4.dp else 0.dp,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,40 +445,86 @@ private fun ProfileBox(
|
|||||||
hasTemplate: Boolean,
|
hasTemplate: Boolean,
|
||||||
onModeChange: (Mode) -> Unit,
|
onModeChange: (Mode) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
Column(modifier = Modifier.padding(vertical = 8.dp)) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.profile)) },
|
headlineContent = {
|
||||||
supportingContent = { Text(mode.text) },
|
Text(
|
||||||
leadingContent = { Icon(Icons.Filled.AccountCircle, null) },
|
text = stringResource(R.string.profile),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
HorizontalDivider(thickness = Dp.Hairline)
|
},
|
||||||
ListItem(headlineContent = {
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = mode.text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.AccountCircle,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
HorizontalDivider(
|
||||||
|
thickness = Dp.Hairline,
|
||||||
|
)
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
) {
|
) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = mode == Mode.Default,
|
selected = mode == Mode.Default,
|
||||||
label = { Text(stringResource(R.string.profile_default)) },
|
|
||||||
onClick = { onModeChange(Mode.Default) },
|
onClick = { onModeChange(Mode.Default) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_default),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
|
||||||
if (hasTemplate) {
|
if (hasTemplate) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = mode == Mode.Template,
|
selected = mode == Mode.Template,
|
||||||
label = { Text(stringResource(R.string.profile_template)) },
|
|
||||||
onClick = { onModeChange(Mode.Template) },
|
onClick = { onModeChange(Mode.Template) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.profile_template),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = mode == Mode.Custom,
|
selected = mode == Mode.Custom,
|
||||||
label = { Text(stringResource(R.string.profile_custom)) },
|
|
||||||
onClick = { onModeChange(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
|
@Composable
|
||||||
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
||||||
|
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) }
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@@ -329,13 +533,14 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
|||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures {
|
detectTapGestures(
|
||||||
|
onLongPress = {
|
||||||
touchPoint = it
|
touchPoint = it
|
||||||
expanded = true
|
expanded = true
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
content()
|
content()
|
||||||
|
|
||||||
val (offsetX, offsetY) = with(density) {
|
val (offsetX, offsetY) = with(density) {
|
||||||
@@ -349,45 +554,70 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) {
|
|||||||
expanded = false
|
expanded = false
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
AppMenuOption(
|
||||||
text = { Text(stringResource(id = R.string.launch_app)) },
|
text = stringResource(id = R.string.launch_app),
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
launchApp(packageName)
|
launchApp(packageName)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.force_stop_app)) },
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.force_stop_app),
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
forceStopApp(packageName)
|
forceStopApp(packageName)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
|
||||||
text = { Text(stringResource(id = R.string.restart_app)) },
|
AppMenuOption(
|
||||||
|
text = stringResource(id = R.string.restart_app),
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
restartApp(packageName)
|
restartApp(packageName)
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppMenuOption(text: String, onClick: () -> Unit) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun AppProfilePreview() {
|
private fun AppProfilePreview() {
|
||||||
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
var profile by remember { mutableStateOf(Natives.Profile("")) }
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Surface {
|
||||||
AppProfileInner(
|
AppProfileInner(
|
||||||
packageName = "icu.nullptr.test",
|
packageName = "icu.nullptr.test",
|
||||||
appLabel = "Test",
|
appLabel = "Test",
|
||||||
appIcon = { Icon(Icons.Filled.Android, null) },
|
appIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Android,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
},
|
||||||
profile = profile,
|
profile = profile,
|
||||||
onProfileChange = {
|
onProfileChange = {
|
||||||
profile = it
|
profile = it
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
|
import androidx.compose.material.icons.filled.Archive
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.outlined.*
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
|
||||||
@@ -21,8 +22,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),
|
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true),
|
||||||
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
|
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, true),
|
||||||
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
|
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension, true),
|
||||||
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -30,7 +37,6 @@ import androidx.compose.ui.input.key.key
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
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.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
@@ -55,7 +61,11 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
var actionResult: Boolean
|
var isActionRunning by rememberSaveable { mutableStateOf(true) }
|
||||||
|
|
||||||
|
BackHandler(enabled = isActionRunning) {
|
||||||
|
// Disable back button if action is running
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (text.isNotEmpty()) {
|
if (text.isNotEmpty()) {
|
||||||
@@ -76,20 +86,17 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
onStderr = {
|
onStderr = {
|
||||||
logContent.append(it).append("\n")
|
logContent.append(it).append("\n")
|
||||||
}
|
}
|
||||||
).let {
|
)
|
||||||
actionResult = it
|
|
||||||
}
|
}
|
||||||
}
|
isActionRunning = false
|
||||||
if (actionResult) navigator.popBackStack()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = dropUnlessResumed {
|
isActionRunning = isActionRunning,
|
||||||
navigator.popBackStack()
|
|
||||||
},
|
|
||||||
onSave = {
|
onSave = {
|
||||||
|
if (!isActionRunning) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
val date = format.format(Date())
|
val date = format.format(Date())
|
||||||
@@ -101,8 +108,21 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
if (!isActionRunning) {
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
text = { Text(text = stringResource(R.string.close)) },
|
||||||
|
icon = { Icon(Icons.Filled.Close, contentDescription = null) },
|
||||||
|
onClick = {
|
||||||
|
navigator.popBackStack()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing,
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
KeyEventBlocker {
|
KeyEventBlocker {
|
||||||
@@ -130,16 +150,14 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
|
private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.action)) },
|
title = { Text(stringResource(R.string.action)) },
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onSave) {
|
IconButton(
|
||||||
|
onClick = onSave,
|
||||||
|
enabled = !isActionRunning
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Save,
|
imageVector = Icons.Filled.Save,
|
||||||
contentDescription = stringResource(id = R.string.save_log),
|
contentDescription = stringResource(id = R.string.save_log),
|
||||||
|
|||||||
@@ -3,27 +3,40 @@ package com.sukisu.ultra.ui.screen
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Error
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.Save
|
import androidx.compose.material.icons.filled.Save
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.dropUnlessResumed
|
|
||||||
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.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -33,10 +46,17 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
import com.sukisu.ultra.ui.component.KeyEventBlocker
|
||||||
import com.sukisu.ultra.ui.util.*
|
import com.sukisu.ultra.ui.util.*
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
|
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author ShirkNeko
|
||||||
|
* @date 2025/5/31.
|
||||||
|
*/
|
||||||
enum class FlashingStatus {
|
enum class FlashingStatus {
|
||||||
FLASHING,
|
FLASHING,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
@@ -45,44 +65,153 @@ enum class FlashingStatus {
|
|||||||
|
|
||||||
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
|
||||||
|
|
||||||
|
// 添加模块安装状态跟踪
|
||||||
|
data class ModuleInstallStatus(
|
||||||
|
val totalModules: Int = 0,
|
||||||
|
val currentModule: Int = 0,
|
||||||
|
val currentModuleName: String = "",
|
||||||
|
val failedModules: MutableList<String> = mutableListOf(),
|
||||||
|
val verifiedModules: MutableList<String> = mutableListOf() // 添加已验证模块列表
|
||||||
|
)
|
||||||
|
|
||||||
|
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus())
|
||||||
|
|
||||||
|
// 存储模块URI和验证状态的映射
|
||||||
|
private var moduleVerificationMap = mutableMapOf<Uri, Boolean>()
|
||||||
|
|
||||||
fun setFlashingStatus(status: FlashingStatus) {
|
fun setFlashingStatus(status: FlashingStatus) {
|
||||||
currentFlashingStatus.value = status
|
currentFlashingStatus.value = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateModuleInstallStatus(
|
||||||
|
totalModules: Int? = null,
|
||||||
|
currentModule: Int? = null,
|
||||||
|
currentModuleName: String? = null,
|
||||||
|
failedModule: String? = null,
|
||||||
|
verifiedModule: String? = null
|
||||||
|
) {
|
||||||
|
val current = moduleInstallStatus.value
|
||||||
|
moduleInstallStatus.value = current.copy(
|
||||||
|
totalModules = totalModules ?: current.totalModules,
|
||||||
|
currentModule = currentModule ?: current.currentModule,
|
||||||
|
currentModuleName = currentModuleName ?: current.currentModuleName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (failedModule != null) {
|
||||||
|
val updatedFailedModules = current.failedModules.toMutableList()
|
||||||
|
updatedFailedModules.add(failedModule)
|
||||||
|
moduleInstallStatus.value = moduleInstallStatus.value.copy(
|
||||||
|
failedModules = updatedFailedModules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifiedModule != null) {
|
||||||
|
val updatedVerifiedModules = current.verifiedModules.toMutableList()
|
||||||
|
updatedVerifiedModules.add(verifiedModule)
|
||||||
|
moduleInstallStatus.value = moduleInstallStatus.value.copy(
|
||||||
|
verifiedModules = updatedVerifiedModules
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) {
|
||||||
|
moduleVerificationMap[uri] = isVerified
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
||||||
|
val context = LocalContext.current
|
||||||
var text by rememberSaveable { mutableStateOf("") }
|
var text by rememberSaveable { mutableStateOf("") }
|
||||||
var tempText: String
|
var tempText: String
|
||||||
val logContent = rememberSaveable { StringBuilder() }
|
val logContent = rememberSaveable { StringBuilder() }
|
||||||
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
var showFloatAction by rememberSaveable { mutableStateOf(false) }
|
||||||
|
// 添加状态跟踪是否已经完成刷写
|
||||||
|
var hasFlashCompleted by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var hasExecuted by rememberSaveable { mutableStateOf(false) }
|
||||||
|
// 更新模块状态管理
|
||||||
|
var hasUpdateExecuted by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var hasUpdateCompleted by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
val viewModel: ModuleViewModel = viewModel()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
val errorCodeString = stringResource(R.string.error_code)
|
||||||
if (text.isNotEmpty()) {
|
val checkLogString = stringResource(R.string.check_log)
|
||||||
|
val logSavedString = stringResource(R.string.log_saved)
|
||||||
|
val installingModuleString = stringResource(R.string.installing_module)
|
||||||
|
|
||||||
|
// 当前模块安装状态
|
||||||
|
val currentStatus = moduleInstallStatus.value
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
LaunchedEffect(flashIt) {
|
||||||
|
when (flashIt) {
|
||||||
|
is FlashIt.FlashModules -> {
|
||||||
|
if (flashIt.currentIndex == 0) {
|
||||||
|
moduleInstallStatus.value = ModuleInstallStatus(
|
||||||
|
totalModules = flashIt.uris.size,
|
||||||
|
currentModule = 1
|
||||||
|
)
|
||||||
|
hasFlashCompleted = false
|
||||||
|
hasExecuted = false
|
||||||
|
moduleVerificationMap.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FlashIt.FlashModuleUpdate -> {
|
||||||
|
hasUpdateCompleted = false
|
||||||
|
hasUpdateExecuted = false
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
hasFlashCompleted = false
|
||||||
|
hasExecuted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理更新模块安装
|
||||||
|
LaunchedEffect(flashIt) {
|
||||||
|
if (flashIt !is FlashIt.FlashModuleUpdate) return@LaunchedEffect
|
||||||
|
if (hasUpdateExecuted || hasUpdateCompleted || text.isNotEmpty()) {
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasUpdateExecuted = true
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
setFlashingStatus(FlashingStatus.FLASHING)
|
setFlashingStatus(FlashingStatus.FLASHING)
|
||||||
flashIt(flashIt, onFinish = { showReboot, code ->
|
|
||||||
|
try {
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
flashModuleUpdate(flashIt.uri, onFinish = { showReboot, code ->
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
text += "Error: exit code = $code.\nPlease save and check the log.\n"
|
text += "$errorCodeString $code.\n$checkLogString\n"
|
||||||
setFlashingStatus(FlashingStatus.FAILED)
|
setFlashingStatus(FlashingStatus.FAILED)
|
||||||
} else {
|
} else {
|
||||||
setFlashingStatus(FlashingStatus.SUCCESS)
|
setFlashingStatus(FlashingStatus.SUCCESS)
|
||||||
|
|
||||||
|
// 处理模块更新成功后的验证标志
|
||||||
|
val isVerified = moduleVerificationMap[flashIt.uri] ?: false
|
||||||
|
ModuleOperationUtils.handleModuleUpdate(context, flashIt.uri, isVerified)
|
||||||
|
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
}
|
}
|
||||||
if (showReboot) {
|
if (showReboot) {
|
||||||
text += "\n\n\n"
|
text += "\n\n\n"
|
||||||
showFloatAction = true
|
showFloatAction = true
|
||||||
}
|
}
|
||||||
|
hasUpdateCompleted = true
|
||||||
}, onStdout = {
|
}, onStdout = {
|
||||||
tempText = "$it\n"
|
tempText = "$it\n"
|
||||||
if (tempText.startsWith("[H[J")) { // clear command
|
if (tempText.startsWith("[H[J")) { // clear command
|
||||||
text = tempText.substring(6)
|
text = tempText.substring(6)
|
||||||
} else {
|
} else {
|
||||||
text += tempText
|
text += tempText
|
||||||
@@ -94,13 +223,128 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 安装但排除更新模块
|
||||||
|
LaunchedEffect(flashIt) {
|
||||||
|
if (flashIt is FlashIt.FlashModuleUpdate) return@LaunchedEffect
|
||||||
|
if (hasExecuted || hasFlashCompleted || text.isNotEmpty()) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
|
||||||
|
hasExecuted = true
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
setFlashingStatus(FlashingStatus.FLASHING)
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
try {
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
val moduleName = getModuleNameFromUri(context, currentUri)
|
||||||
|
updateModuleInstallStatus(
|
||||||
|
currentModuleName = moduleName
|
||||||
|
)
|
||||||
|
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName)
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
} catch (_: Exception) {
|
||||||
|
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module")
|
||||||
|
logContent.append(text).append("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flashIt(flashIt, onFinish = { showReboot, code ->
|
||||||
|
if (code != 0) {
|
||||||
|
text += "$errorCodeString $code.\n$checkLogString\n"
|
||||||
|
setFlashingStatus(FlashingStatus.FAILED)
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
updateModuleInstallStatus(
|
||||||
|
failedModule = moduleInstallStatus.value.currentModuleName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setFlashingStatus(FlashingStatus.SUCCESS)
|
||||||
|
|
||||||
|
// 处理模块安装成功后的验证标志
|
||||||
|
when (flashIt) {
|
||||||
|
is FlashIt.FlashModule -> {
|
||||||
|
val isVerified = moduleVerificationMap[flashIt.uri] ?: false
|
||||||
|
ModuleOperationUtils.handleModuleInstallSuccess(context, flashIt.uri, isVerified)
|
||||||
|
if (isVerified) {
|
||||||
|
updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is FlashIt.FlashModules -> {
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
val isVerified = moduleVerificationMap[currentUri] ?: false
|
||||||
|
ModuleOperationUtils.handleModuleInstallSuccess(context, currentUri, isVerified)
|
||||||
|
if (isVerified) {
|
||||||
|
updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
|
}
|
||||||
|
if (showReboot) {
|
||||||
|
text += "\n\n\n"
|
||||||
|
showFloatAction = true
|
||||||
|
}
|
||||||
|
|
||||||
|
hasFlashCompleted = true
|
||||||
|
|
||||||
|
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
|
||||||
|
val nextFlashIt = flashIt.copy(
|
||||||
|
currentIndex = flashIt.currentIndex + 1
|
||||||
|
)
|
||||||
|
scope.launch {
|
||||||
|
kotlinx.coroutines.delay(500)
|
||||||
|
navigator.navigate(FlashScreenDestination(nextFlashIt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, onStdout = {
|
||||||
|
tempText = "$it\n"
|
||||||
|
if (tempText.startsWith("[H[J")) { // clear command
|
||||||
|
text = tempText.substring(6)
|
||||||
|
} else {
|
||||||
|
text += tempText
|
||||||
|
}
|
||||||
|
logContent.append(it).append("\n")
|
||||||
|
}, onStderr = {
|
||||||
|
logContent.append(it).append("\n")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val onBack: () -> Unit = {
|
||||||
|
val canGoBack = when (flashIt) {
|
||||||
|
is FlashIt.FlashModuleUpdate -> currentFlashingStatus.value != FlashingStatus.FLASHING
|
||||||
|
else -> currentFlashingStatus.value != FlashingStatus.FLASHING
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canGoBack) {
|
||||||
|
if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) {
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
navigator.navigate(ModuleScreenDestination)
|
||||||
|
} else {
|
||||||
|
viewModel.markNeedRefresh()
|
||||||
|
viewModel.fetchModuleList()
|
||||||
|
navigator.popBackStack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler(enabled = true) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
currentFlashingStatus.value,
|
currentFlashingStatus.value,
|
||||||
onBack = dropUnlessResumed {
|
currentStatus,
|
||||||
navigator.popBackStack()
|
onBack = onBack,
|
||||||
},
|
|
||||||
onSave = {
|
onSave = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
|
||||||
@@ -110,7 +354,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
"KernelSU_install_log_${date}.log"
|
"KernelSU_install_log_${date}.log"
|
||||||
)
|
)
|
||||||
file.writeText(logContent.toString())
|
file.writeText(logContent.toString())
|
||||||
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
|
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
@@ -118,8 +362,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (showFloatAction) {
|
if (showFloatAction) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
val reboot = stringResource(id = R.string.reboot)
|
|
||||||
ExtendedFloatingActionButton(
|
ExtendedFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -128,48 +370,295 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = { Icon(Icons.Filled.Refresh, reboot) },
|
icon = {
|
||||||
text = { Text(text = reboot) },
|
Icon(
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
Icons.Filled.Refresh,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentDescription = stringResource(id = R.string.reboot)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(id = R.string.reboot))
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
expanded = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
containerColor = MaterialTheme.colorScheme.background
|
||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
KeyEventBlocker {
|
KeyEventBlocker {
|
||||||
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
it.key == Key.VolumeDown || it.key == Key.VolumeUp
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize(1f)
|
.fillMaxSize(1f)
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
.verticalScroll(scrollState),
|
) {
|
||||||
|
if (flashIt is FlashIt.FlashModules) {
|
||||||
|
ModuleInstallProgressBar(
|
||||||
|
currentIndex = flashIt.currentIndex + 1,
|
||||||
|
totalCount = flashIt.uris.size,
|
||||||
|
currentModuleName = currentStatus.currentModuleName,
|
||||||
|
status = currentFlashingStatus.value,
|
||||||
|
failedModules = currentStatus.failedModules
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.weight(1f)
|
||||||
|
.verticalScroll(scrollState)
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(text) {
|
LaunchedEffect(text) {
|
||||||
scrollState.animateScrollTo(scrollState.maxValue)
|
scrollState.animateScrollTo(scrollState.maxValue)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(8.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
text = text,
|
text = text,
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
fontFamily = FontFamily.Monospace,
|
fontFamily = FontFamily.Monospace,
|
||||||
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示模块安装进度条和状态
|
||||||
|
@Composable
|
||||||
|
fun ModuleInstallProgressBar(
|
||||||
|
currentIndex: Int,
|
||||||
|
totalCount: Int,
|
||||||
|
currentModuleName: String,
|
||||||
|
status: FlashingStatus,
|
||||||
|
failedModules: List<String>
|
||||||
|
) {
|
||||||
|
val progressColor = when(status) {
|
||||||
|
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
|
||||||
|
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
|
||||||
|
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
val progress = animateFloatAsState(
|
||||||
|
targetValue = currentIndex.toFloat() / totalCount.toFloat(),
|
||||||
|
label = "InstallProgress"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
// 模块名称和进度
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = currentModuleName.ifEmpty { stringResource(R.string.module) },
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "$currentIndex/$totalCount",
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 进度条
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress.value },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(8.dp),
|
||||||
|
color = progressColor,
|
||||||
|
trackColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
// 失败模块列表
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = failedModules.isNotEmpty(),
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Error,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.error,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.module_failed_count, failedModules.size),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// 失败模块列表
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
|
||||||
|
shape = MaterialTheme.shapes.small
|
||||||
|
)
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
failedModules.forEach { moduleName ->
|
||||||
|
Text(
|
||||||
|
text = "• $moduleName",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun TopBar(
|
||||||
|
status: FlashingStatus,
|
||||||
|
moduleStatus: ModuleInstallStatus = ModuleInstallStatus(),
|
||||||
|
onBack: () -> Unit,
|
||||||
|
onSave: () -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
|
) {
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
|
colorScheme.surfaceContainerLow
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
|
val statusColor = when(status) {
|
||||||
|
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
|
||||||
|
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
|
||||||
|
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
when (status) {
|
||||||
|
FlashingStatus.FLASHING -> R.string.flashing
|
||||||
|
FlashingStatus.SUCCESS -> R.string.flash_success
|
||||||
|
FlashingStatus.FAILED -> R.string.flash_failed
|
||||||
|
}
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = statusColor
|
||||||
|
)
|
||||||
|
|
||||||
|
if (moduleStatus.failedModules.isNotEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.module_failed_count, moduleStatus.failedModules.size),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onBack) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
|
),
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onSave) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Save,
|
||||||
|
contentDescription = stringResource(id = R.string.save_log),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
if (uri == Uri.EMPTY) {
|
||||||
|
return@withContext context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
if (!ModuleUtils.isUriAccessible(context, uri)) {
|
||||||
|
return@withContext context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
ModuleUtils.extractModuleName(context, uri)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
context.getString(R.string.unknown_module)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
sealed class FlashIt : Parcelable {
|
sealed class FlashIt : Parcelable {
|
||||||
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
|
||||||
data class FlashModule(val uri: Uri) : FlashIt()
|
data class FlashModule(val uri: Uri) : FlashIt()
|
||||||
|
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
|
||||||
|
data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新
|
||||||
data object FlashRestore : FlashIt()
|
data object FlashRestore : FlashIt()
|
||||||
data object FlashUninstall : FlashIt()
|
data object FlashUninstall : FlashIt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 模块更新刷写
|
||||||
|
fun flashModuleUpdate(
|
||||||
|
uri: Uri,
|
||||||
|
onFinish: (Boolean, Int) -> Unit,
|
||||||
|
onStdout: (String) -> Unit,
|
||||||
|
onStderr: (String) -> Unit
|
||||||
|
) {
|
||||||
|
flashModule(uri, onFinish, onStdout, onStderr)
|
||||||
|
}
|
||||||
|
|
||||||
fun flashIt(
|
fun flashIt(
|
||||||
flashIt: FlashIt,
|
flashIt: FlashIt,
|
||||||
onFinish: (Boolean, Int) -> Unit,
|
onFinish: (Boolean, Int) -> Unit,
|
||||||
@@ -186,49 +675,25 @@ fun flashIt(
|
|||||||
onStderr
|
onStderr
|
||||||
)
|
)
|
||||||
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
|
||||||
|
is FlashIt.FlashModules -> {
|
||||||
|
if (flashIt.uris.isEmpty() || flashIt.currentIndex >= flashIt.uris.size) {
|
||||||
|
onFinish(false, 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentUri = flashIt.uris[flashIt.currentIndex]
|
||||||
|
onStdout("\n")
|
||||||
|
|
||||||
|
flashModule(currentUri, onFinish, onStdout, onStderr)
|
||||||
|
}
|
||||||
|
is FlashIt.FlashModuleUpdate -> {
|
||||||
|
onFinish(false, 0)
|
||||||
|
}
|
||||||
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
|
||||||
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
private fun TopBar(
|
|
||||||
status: FlashingStatus,
|
|
||||||
onBack: () -> Unit = {},
|
|
||||||
onSave: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
|
||||||
) {
|
|
||||||
TopAppBar(
|
|
||||||
title = {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
when (status) {
|
|
||||||
FlashingStatus.FLASHING -> R.string.flashing
|
|
||||||
FlashingStatus.SUCCESS -> R.string.flash_success
|
|
||||||
FlashingStatus.FAILED -> R.string.flash_failed
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
navigationIcon = {
|
|
||||||
IconButton(
|
|
||||||
onClick = onBack
|
|
||||||
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
|
|
||||||
},
|
|
||||||
actions = {
|
|
||||||
IconButton(onClick = onSave) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Filled.Save,
|
|
||||||
contentDescription = "Localized description"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun FlashScreenPreview() {
|
fun FlashScreenPreview() {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -15,27 +15,66 @@ import androidx.compose.animation.shrinkVertically
|
|||||||
import androidx.compose.foundation.LocalIndication
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
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.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.material.icons.filled.FileUpload
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
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.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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.nestedscroll.nestedScroll
|
||||||
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.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
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 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.ListOption
|
||||||
|
import com.maxkeppeler.sheets.list.models.ListSelection
|
||||||
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.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
|
import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
@@ -43,18 +82,22 @@ import com.sukisu.ultra.ui.component.DialogHandle
|
|||||||
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
import com.sukisu.ultra.ui.component.SlotSelectionDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
import com.sukisu.ultra.ui.component.rememberConfirmDialog
|
||||||
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
import com.sukisu.ultra.ui.component.rememberCustomDialog
|
||||||
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.flash.HorizonKernelState
|
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
|
||||||
import com.sukisu.ultra.flash.HorizonKernelWorker
|
|
||||||
import com.sukisu.ultra.ui.theme.CardConfig
|
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
|
||||||
import com.sukisu.ultra.ui.theme.getCardColors
|
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
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
import com.sukisu.ultra.ui.theme.getCardElevation
|
import com.sukisu.ultra.ui.theme.getCardElevation
|
||||||
import com.sukisu.ultra.ui.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author ShirkNeko
|
||||||
* @date 2024/3/12.
|
* @date 2025/5/31.
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@@ -66,14 +109,11 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
var showRebootDialog by remember { mutableStateOf(false) }
|
var showRebootDialog by remember { mutableStateOf(false) }
|
||||||
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
var showSlotSelectionDialog by remember { mutableStateOf(false) }
|
||||||
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
val horizonKernelState = remember { HorizonKernelState() }
|
val kernelVersion = getKernelVersion()
|
||||||
val flashState by horizonKernelState.state.collectAsState()
|
val isGKI = kernelVersion.isGKI()
|
||||||
|
val isAbDevice = isAbDevice()
|
||||||
val summary = stringResource(R.string.horizon_kernel_summary)
|
val summary = stringResource(R.string.horizon_kernel_summary)
|
||||||
|
|
||||||
val onFlashComplete = {
|
|
||||||
showRebootDialog = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRebootDialog) {
|
if (showRebootDialog) {
|
||||||
RebootDialog(
|
RebootDialog(
|
||||||
show = true,
|
show = true,
|
||||||
@@ -98,14 +138,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
when (method) {
|
when (method) {
|
||||||
is InstallMethod.HorizonKernel -> {
|
is InstallMethod.HorizonKernel -> {
|
||||||
method.uri?.let { uri ->
|
method.uri?.let { uri ->
|
||||||
val worker = HorizonKernelWorker(
|
navigator.navigate(
|
||||||
context = context,
|
KernelFlashScreenDestination(
|
||||||
state = horizonKernelState,
|
kernelUri = uri,
|
||||||
slot = method.slot
|
selectedSlot = method.slot
|
||||||
|
)
|
||||||
)
|
)
|
||||||
worker.uri = uri
|
|
||||||
worker.setOnFlashCompleteListener(onFlashComplete)
|
|
||||||
worker.start()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -123,7 +161,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
|
|
||||||
// 槽位选择
|
// 槽位选择
|
||||||
SlotSelectionDialog(
|
SlotSelectionDialog(
|
||||||
show = showSlotSelectionDialog,
|
show = showSlotSelectionDialog && isAbDevice,
|
||||||
onDismiss = { showSlotSelectionDialog = false },
|
onDismiss = { showSlotSelectionDialog = false },
|
||||||
onSlotSelected = { slot ->
|
onSlotSelected = { slot ->
|
||||||
showSlotSelectionDialog = false
|
showSlotSelectionDialog = false
|
||||||
@@ -133,7 +171,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
summary = summary
|
summary = summary
|
||||||
)
|
)
|
||||||
installMethod = horizonMethod
|
installMethod = horizonMethod
|
||||||
onInstall()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,28 +186,13 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val onClickNext = {
|
val onClickNext = {
|
||||||
if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) {
|
if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank() && installMethod !is InstallMethod.HorizonKernel) {
|
||||||
selectKmiDialog.show()
|
selectKmiDialog.show()
|
||||||
} else {
|
} else {
|
||||||
onInstall()
|
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())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
|
|
||||||
@@ -178,7 +200,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(
|
||||||
onBack = { navigator.popBackStack() },
|
onBack = { navigator.popBackStack() },
|
||||||
onLkmUpload = onLkmUpload,
|
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -191,59 +212,97 @@ fun InstallScreen(navigator: DestinationsNavigator) {
|
|||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(top = 12.dp)
|
||||||
) {
|
) {
|
||||||
SelectInstallMethod(
|
SelectInstallMethod(
|
||||||
|
isGKI = isGKI,
|
||||||
onSelected = { method ->
|
onSelected = { method ->
|
||||||
if (method is InstallMethod.HorizonKernel && method.uri != null && method.slot == null) {
|
if (method is InstallMethod.HorizonKernel && method.uri != null) {
|
||||||
|
if (isAbDevice) {
|
||||||
tempKernelUri = method.uri
|
tempKernelUri = method.uri
|
||||||
showSlotSelectionDialog = true
|
showSlotSelectionDialog = true
|
||||||
} else {
|
} else {
|
||||||
installMethod = method
|
installMethod = method
|
||||||
}
|
}
|
||||||
horizonKernelState.reset()
|
} else {
|
||||||
|
installMethod = method
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
|
|
||||||
enter = fadeIn() + expandVertically(),
|
|
||||||
exit = fadeOut() + shrinkVertically()
|
|
||||||
) {
|
|
||||||
HorizonKernelFlashProgress(flashState)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
(lkmSelection as? LkmSelection.LkmUri)?.let {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
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(
|
text = stringResource(
|
||||||
id = R.string.selected_lkm,
|
id = R.string.selected_lkm,
|
||||||
it.uri.lastPathSegment ?: "(file)"
|
it.uri.lastPathSegment ?: "(file)"
|
||||||
)
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
(installMethod as? InstallMethod.HorizonKernel)?.let { method ->
|
||||||
if (method.slot != null) {
|
if (method.slot != null) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
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(
|
text = stringResource(
|
||||||
id = R.string.selected_slot,
|
id = R.string.selected_slot,
|
||||||
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
if (method.slot == "a") stringResource(id = R.string.slot_a)
|
||||||
else stringResource(id = R.string.slot_b)
|
else stringResource(id = R.string.slot_b)
|
||||||
)
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Button(
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = installMethod != null && !flashState.isFlashing,
|
enabled = installMethod != null,
|
||||||
onClick = onClickNext
|
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(
|
Text(
|
||||||
stringResource(id = R.string.install_next),
|
stringResource(id = R.string.install_next),
|
||||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,13 +364,20 @@ sealed class InstallMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
private fun SelectInstallMethod(
|
||||||
|
isGKI: Boolean = false,
|
||||||
|
onSelected: (InstallMethod) -> Unit = {}
|
||||||
|
) {
|
||||||
val rootAvailable = rootAvailable()
|
val rootAvailable = rootAvailable()
|
||||||
val isAbDevice = isAbDevice()
|
val isAbDevice = isAbDevice()
|
||||||
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary)
|
||||||
val selectFileTip = stringResource(
|
val selectFileTip = stringResource(
|
||||||
id = R.string.select_file_tip,
|
id = R.string.select_file_tip,
|
||||||
if (isInitBoot()) "init_boot" else "boot"
|
if (isInitBoot()) {
|
||||||
|
"init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}"
|
||||||
|
} else {
|
||||||
|
"boot"
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
val radioOptions = mutableListOf<InstallMethod>(
|
val radioOptions = mutableListOf<InstallMethod>(
|
||||||
@@ -335,8 +401,16 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|||||||
if (it.resultCode == Activity.RESULT_OK) {
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
it.data?.data?.let { uri ->
|
it.data?.data?.let { uri ->
|
||||||
val option = when (currentSelectingMethod) {
|
val option = when (currentSelectingMethod) {
|
||||||
is InstallMethod.SelectFile -> InstallMethod.SelectFile(uri, summary = selectFileTip)
|
is InstallMethod.SelectFile -> InstallMethod.SelectFile(
|
||||||
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(uri, summary = horizonKernelSummary)
|
uri,
|
||||||
|
summary = selectFileTip
|
||||||
|
)
|
||||||
|
|
||||||
|
is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel(
|
||||||
|
uri,
|
||||||
|
summary = horizonKernelSummary
|
||||||
|
)
|
||||||
|
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
option?.let {
|
option?.let {
|
||||||
@@ -364,22 +438,90 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|||||||
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> {
|
||||||
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
type = "application/*"
|
type = "application/*"
|
||||||
putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/octet-stream", "application/zip"))
|
putExtra(
|
||||||
|
Intent.EXTRA_MIME_TYPES,
|
||||||
|
arrayOf("application/octet-stream", "application/zip")
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
is InstallMethod.DirectInstall -> {
|
is InstallMethod.DirectInstall -> {
|
||||||
selectedOption = option
|
selectedOption = option
|
||||||
onSelected(option)
|
onSelected(option)
|
||||||
}
|
}
|
||||||
|
|
||||||
is InstallMethod.DirectInstallToInactiveSlot -> {
|
is InstallMethod.DirectInstallToInactiveSlot -> {
|
||||||
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
confirmDialog.showConfirm(dialogTitle, dialogContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
var lkmExpanded by remember { mutableStateOf(false) }
|
||||||
radioOptions.forEach { option ->
|
var gkiExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
// LKM 安装/修补
|
||||||
|
if (isGKI) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.AutoFixHigh,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.Lkm_install_methods),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
lkmExpanded = !lkmExpanded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = lkmExpanded,
|
||||||
|
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() }
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -391,33 +533,149 @@ private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) {
|
|||||||
indication = LocalIndication.current,
|
indication = LocalIndication.current,
|
||||||
interactionSource = interactionSource
|
interactionSource = interactionSource
|
||||||
)
|
)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = option.javaClass == selectedOption?.javaClass,
|
selected = option.javaClass == selectedOption?.javaClass,
|
||||||
onClick = { onClick(option) },
|
onClick = null,
|
||||||
interactionSource = interactionSource
|
interactionSource = interactionSource,
|
||||||
|
colors = RadioButtonDefaults.colors(
|
||||||
|
selectedColor = MaterialTheme.colorScheme.primary,
|
||||||
|
unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 12.dp)
|
modifier = Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(id = option.label),
|
text = stringResource(id = option.label),
|
||||||
fontSize = MaterialTheme.typography.titleMedium.fontSize,
|
style = MaterialTheme.typography.bodyLarge
|
||||||
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
|
|
||||||
fontStyle = MaterialTheme.typography.titleMedium.fontStyle
|
|
||||||
)
|
)
|
||||||
option.summary?.let {
|
option.summary?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
fontSize = MaterialTheme.typography.bodySmall.fontSize,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
fontStyle = MaterialTheme.typography.bodySmall.fontStyle
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// anykernel3 刷写
|
||||||
|
if (rootAvailable) {
|
||||||
|
ElevatedCard(
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
elevation = getCardElevation(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 12.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.FileUpload,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.GKI_install_methods),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
gkiExpanded = !gkiExpanded
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = gkiExpanded,
|
||||||
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -427,77 +685,32 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|||||||
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
val supportedKmi by produceState(initialValue = emptyList<String>()) {
|
||||||
value = getSupportedKmis()
|
value = getSupportedKmis()
|
||||||
}
|
}
|
||||||
val listOptions = supportedKmi.map { value ->
|
val options = supportedKmi.map { value ->
|
||||||
ListOption(
|
ListOption(
|
||||||
titleText = value,
|
titleText = value
|
||||||
subtitleText = null,
|
|
||||||
icon = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selection: String? = null
|
var selection by remember { mutableStateOf<String?>(null) }
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog(
|
MaterialTheme(
|
||||||
onDismissRequest = {
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
dismiss()
|
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
},
|
)
|
||||||
title = {
|
|
||||||
Text(text = stringResource(R.string.select_kmi))
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
listOptions.forEachIndexed { index, option ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
selection = supportedKmi[index]
|
|
||||||
}
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
) {
|
) {
|
||||||
Column {
|
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
|
||||||
Text(text = option.titleText)
|
|
||||||
option.subtitleText?.let {
|
|
||||||
Text(
|
|
||||||
text = it,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
if (selection != null) {
|
|
||||||
onSelected(selection)
|
onSelected(selection)
|
||||||
}
|
}, onCloseRequest = {
|
||||||
dismiss()
|
dismiss()
|
||||||
|
}), header = Header.Default(
|
||||||
|
title = stringResource(R.string.select_kmi),
|
||||||
|
), selection = ListSelection.Single(
|
||||||
|
showRadioButtons = true,
|
||||||
|
options = options,
|
||||||
|
) { _, option ->
|
||||||
|
selection = option.titleText
|
||||||
|
})
|
||||||
}
|
}
|
||||||
) {
|
|
||||||
Text(text = stringResource(android.R.string.ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(android.R.string.cancel))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
|
||||||
shape = MaterialTheme.shapes.medium,
|
|
||||||
tonalElevation = getCardElevation()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,26 +718,33 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
onLkmUpload: () -> Unit = {},
|
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
|
colorScheme.surfaceContainerLow
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
|
val cardAlpha = cardAlpha
|
||||||
|
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.install)) },
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.install),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
),
|
),
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
Icon(
|
||||||
}
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
},
|
contentDescription = stringResource(R.string.back)
|
||||||
actions = {
|
)
|
||||||
IconButton(onClick = onLkmUpload) {
|
|
||||||
Icon(Icons.Filled.FileUpload, contentDescription = null)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
windowInsets = WindowInsets.safeDrawing.only(
|
windowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
package com.sukisu.ultra.ui.screen
|
||||||
|
|
||||||
import android.app.Activity.RESULT_OK
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -9,12 +8,14 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
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.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
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.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.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -22,7 +23,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
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.navigation.DestinationsNavigator
|
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.ui.component.*
|
import com.sukisu.ultra.ui.component.*
|
||||||
@@ -32,10 +32,10 @@ import com.sukisu.ultra.ui.util.*
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.net.*
|
import java.net.*
|
||||||
|
import android.app.Activity
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KPM 管理界面
|
* KPM 管理界面
|
||||||
@@ -46,18 +46,15 @@ import java.net.*
|
|||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun KpmScreen(
|
fun KpmScreen(
|
||||||
navigator: DestinationsNavigator,
|
|
||||||
viewModel: KpmViewModel = viewModel()
|
viewModel: KpmViewModel = viewModel()
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val snackBarHost = remember { SnackbarHostState() }
|
val snackBarHost = remember { SnackbarHostState() }
|
||||||
val confirmDialog = rememberConfirmDialog()
|
val confirmDialog = rememberConfirmDialog()
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
val listState = rememberLazyListState()
|
||||||
} else {
|
val fabVisible by rememberFabVisibilityState(listState)
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
|
||||||
val moduleFileName = module.id
|
val moduleFileName = module.id
|
||||||
@@ -86,33 +83,47 @@ fun KpmScreen(
|
|||||||
LaunchedEffect(tempFileForInstall) {
|
LaunchedEffect(tempFileForInstall) {
|
||||||
tempFileForInstall?.let { tempFile ->
|
tempFileForInstall?.let { tempFile ->
|
||||||
try {
|
try {
|
||||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
val shell = getRootShell()
|
||||||
val process = Runtime.getRuntime().exec(command)
|
val command = "strings ${tempFile.absolutePath} | grep 'name='"
|
||||||
val inputStream = process.inputStream
|
val result = shell.newJob().add(command).to(ArrayList(), null).exec()
|
||||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
if (result.isSuccess) {
|
||||||
var line: String?
|
for (line in result.out) {
|
||||||
while (reader.readLine().also { line = it } != null) {
|
if (line.startsWith("name=")) {
|
||||||
if (line!!.startsWith("name=")) {
|
|
||||||
moduleName = line.substringAfter("name=").trim()
|
moduleName = line.substringAfter("name=").trim()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process.waitFor()
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
|
Log.e("KsuCli", "Failed to get module name: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
dismiss()
|
dismiss()
|
||||||
tempFileForInstall?.delete()
|
tempFileForInstall?.delete()
|
||||||
tempFileForInstall = null
|
tempFileForInstall = null
|
||||||
},
|
},
|
||||||
title = { Text(kpmInstallMode) },
|
title = {
|
||||||
text = { moduleName?.let { Text(stringResource(R.string.kpm_install_mode_description, it)) } },
|
Text(
|
||||||
confirmButton = {
|
text = kpmInstallMode,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
Column {
|
Column {
|
||||||
|
moduleName?.let {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_install_mode_description, it),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -129,11 +140,17 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
tempFileForInstall = null
|
tempFileForInstall = null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Download,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||||
|
)
|
||||||
Text(kpmInstallModeLoad)
|
Text(kpmInstallModeLoad)
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -150,13 +167,27 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
tempFileForInstall = null
|
tempFileForInstall = null
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Inventory,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp).padding(end = 4.dp)
|
||||||
|
)
|
||||||
Text(kpmInstallModeEmbed)
|
Text(kpmInstallModeEmbed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -167,13 +198,15 @@ fun KpmScreen(
|
|||||||
Text(cancel)
|
Text(cancel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectPatchLauncher = rememberLauncherForActivityResult(
|
val selectPatchLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.StartActivityForResult()
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
) { result ->
|
) { result ->
|
||||||
if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult
|
if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
|
||||||
|
|
||||||
@@ -236,17 +269,22 @@ fun KpmScreen(
|
|||||||
onClearClick = { viewModel.search = "" },
|
onClearClick = { viewModel.search = "" },
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
dropdownContent = {
|
dropdownContent = {
|
||||||
IconButton(onClick = { viewModel.fetchModuleList() }) {
|
IconButton(
|
||||||
|
onClick = { viewModel.fetchModuleList() }
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Refresh,
|
imageVector = Icons.Filled.Refresh,
|
||||||
contentDescription = stringResource(R.string.refresh)
|
contentDescription = stringResource(R.string.refresh),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
ExtendedFloatingActionButton(
|
AnimatedFab(visible = fabVisible) {
|
||||||
|
FloatingActionButton(
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
onClick = {
|
onClick = {
|
||||||
selectPatchLauncher.launch(
|
selectPatchLauncher.launch(
|
||||||
Intent(Intent.ACTION_GET_CONTENT).apply {
|
Intent(Intent.ACTION_GET_CONTENT).apply {
|
||||||
@@ -254,21 +292,28 @@ fun KpmScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
icon = {
|
content = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Add,
|
painter = painterResource(id = R.drawable.package_import),
|
||||||
contentDescription = stringResource(R.string.kpm_install)
|
contentDescription = null
|
||||||
)
|
)
|
||||||
},
|
}
|
||||||
text = { Text(stringResource(R.string.kpm_install)) },
|
|
||||||
containerColor = cardColor.copy(alpha = 1f),
|
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
contentWindowInsets = WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
|
||||||
|
),
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) }
|
snackbarHost = { SnackbarHost(snackBarHost) }
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(modifier = Modifier.padding(padding)) {
|
Column(modifier = Modifier.padding(padding)) {
|
||||||
if (!isNoticeClosed) {
|
if (!isNoticeClosed) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -276,37 +321,65 @@ fun KpmScreen(
|
|||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Info,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.kernel_module_notice),
|
text = stringResource(R.string.kernel_module_notice),
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
textAlign = TextAlign.Center
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
IconButton(onClick = {
|
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
isNoticeClosed = true
|
isNoticeClosed = true
|
||||||
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
sharedPreferences.edit { putBoolean("is_notice_closed", true) }
|
||||||
}) {
|
},
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Close,
|
imageVector = Icons.Filled.Close,
|
||||||
contentDescription = stringResource(R.string.close_notice)
|
contentDescription = stringResource(R.string.close_notice)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (viewModel.moduleList.isEmpty()) {
|
if (viewModel.moduleList.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center
|
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(
|
Text(
|
||||||
stringResource(R.string.kpm_empty),
|
stringResource(R.string.kpm_empty),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
items(viewModel.moduleList) { module ->
|
items(viewModel.moduleList) { module ->
|
||||||
@@ -351,18 +424,17 @@ private suspend fun handleModuleInstall(
|
|||||||
) {
|
) {
|
||||||
var moduleId: String? = null
|
var moduleId: String? = null
|
||||||
try {
|
try {
|
||||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='")
|
val shell = getRootShell()
|
||||||
val process = Runtime.getRuntime().exec(command)
|
val command = "strings ${tempFile.absolutePath} | grep 'name='"
|
||||||
val inputStream = process.inputStream
|
val result = shell.newJob().add(command).to(ArrayList(), null).exec()
|
||||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
if (result.isSuccess) {
|
||||||
var line: String?
|
for (line in result.out) {
|
||||||
while (reader.readLine().also { line = it } != null) {
|
if (line.startsWith("name=")) {
|
||||||
if (line!!.startsWith("name=")) {
|
|
||||||
moduleId = line.substringAfter("name=").trim()
|
moduleId = line.substringAfter("name=").trim()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process.waitFor()
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
|
Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -381,8 +453,9 @@ private suspend fun handleModuleInstall(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (isEmbed) {
|
if (isEmbed) {
|
||||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor()
|
val shell = getRootShell()
|
||||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor()
|
shell.newJob().add("mkdir -p /data/adb/kpm").exec()
|
||||||
|
shell.newJob().add("cp ${tempFile.absolutePath} $targetPath").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
val loadResult = loadKpmModule(tempFile.absolutePath)
|
val loadResult = loadKpmModule(tempFile.absolutePath)
|
||||||
@@ -426,8 +499,9 @@ private suspend fun handleModuleUninstall(
|
|||||||
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
val moduleFilePath = "/data/adb/kpm/$moduleFileName"
|
||||||
|
|
||||||
val fileExists = try {
|
val fileExists = try {
|
||||||
val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0
|
val shell = getRootShell()
|
||||||
result
|
val result = shell.newJob().add("ls /data/adb/kpm/$moduleFileName").exec()
|
||||||
|
result.isSuccess
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e)
|
||||||
snackBarHost.showSnackbar(
|
snackBarHost.showSnackbar(
|
||||||
@@ -436,6 +510,7 @@ private suspend fun handleModuleUninstall(
|
|||||||
)
|
)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
val confirmResult = confirmDialog.awaitConfirm(
|
val confirmResult = confirmDialog.awaitConfirm(
|
||||||
title = confirmTitle,
|
title = confirmTitle,
|
||||||
content = confirmContent,
|
content = confirmContent,
|
||||||
@@ -456,7 +531,8 @@ private suspend fun handleModuleUninstall(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileExists) {
|
if (fileExists) {
|
||||||
Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor()
|
val shell = getRootShell()
|
||||||
|
shell.newJob().add("rm $moduleFilePath").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.fetchModuleList()
|
viewModel.fetchModuleList()
|
||||||
@@ -489,13 +565,27 @@ private fun KpmModuleItem(
|
|||||||
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { viewModel.hideInputDialog() },
|
onDismissRequest = { viewModel.hideInputDialog() },
|
||||||
title = { Text(stringResource(R.string.kpm_control)) },
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_control),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
},
|
||||||
text = {
|
text = {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.inputArgs,
|
value = viewModel.inputArgs,
|
||||||
onValueChange = { viewModel.updateInputArgs(it) },
|
onValueChange = { viewModel.updateInputArgs(it) },
|
||||||
label = { Text(stringResource(R.string.kpm_args)) },
|
label = {
|
||||||
placeholder = { Text(module.args) }
|
Text(
|
||||||
|
text = stringResource(R.string.kpm_args),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = module.args,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
@@ -512,23 +602,28 @@ private fun KpmModuleItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.confirm))
|
Text(
|
||||||
|
text = stringResource(R.string.confirm),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
TextButton(onClick = { viewModel.hideInputDialog() }) {
|
||||||
Text(stringResource(R.string.cancel))
|
Text(
|
||||||
}
|
text = stringResource(R.string.cancel),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
shape = MaterialTheme.shapes.extraLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ElevatedCard(
|
Card(
|
||||||
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer),
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = getCardElevation())
|
elevation = getCardElevation()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(16.dp)
|
modifier = Modifier.padding(20.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -538,54 +633,68 @@ private fun KpmModuleItem(
|
|||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = module.name,
|
text = module.name,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
text = "${stringResource(R.string.kpm_version)}: ${module.version}",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
text = "${stringResource(R.string.kpm_author)}: ${module.author}",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
text = "${stringResource(R.string.kpm_args)}: ${module.args}",
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = module.description,
|
text = module.description,
|
||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
FilledTonalButton(
|
Button(
|
||||||
onClick = { viewModel.showInputDialog(module.id) },
|
onClick = { viewModel.showInputDialog(module.id) },
|
||||||
enabled = module.hasAction
|
enabled = module.hasAction,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Settings,
|
imageVector = Icons.Filled.Settings,
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(stringResource(R.string.kpm_control))
|
Text(stringResource(R.string.kpm_control))
|
||||||
}
|
}
|
||||||
|
|
||||||
FilledTonalButton(
|
Button(
|
||||||
onClick = onUninstall
|
onClick = onUninstall,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Delete,
|
imageVector = Icons.Filled.Delete,
|
||||||
contentDescription = null
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(stringResource(R.string.kpm_uninstall))
|
Text(stringResource(R.string.kpm_uninstall))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,29 +703,31 @@ private fun KpmModuleItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun checkStringsCommand(tempFile: File): Int {
|
private fun checkStringsCommand(tempFile: File): Int {
|
||||||
val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='")
|
val shell = getRootShell()
|
||||||
val process = Runtime.getRuntime().exec(command)
|
val command = "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='"
|
||||||
val inputStream = process.inputStream
|
val result = shell.newJob().add(command).to(ArrayList(), null).exec()
|
||||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
|
||||||
var line: String?
|
if (!result.isSuccess) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
var matchCount = 0
|
var matchCount = 0
|
||||||
val keywords = listOf("name=", "version=", "license=", "author=")
|
val keywords = listOf("name=", "version=", "license=", "author=")
|
||||||
var nameExists = false
|
var nameExists = false
|
||||||
|
|
||||||
while (reader.readLine().also { line = it } != null) {
|
for (line in result.out) {
|
||||||
if (!nameExists && line!!.startsWith("name=")) {
|
if (!nameExists && line.startsWith("name=")) {
|
||||||
nameExists = true
|
nameExists = true
|
||||||
matchCount++
|
matchCount++
|
||||||
} else if (nameExists) {
|
} else if (nameExists) {
|
||||||
for (keyword in keywords) {
|
for (keyword in keywords) {
|
||||||
if (line!!.startsWith(keyword)) {
|
if (line.startsWith(keyword)) {
|
||||||
matchCount++
|
matchCount++
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
process.waitFor()
|
|
||||||
|
|
||||||
return if (nameExists) matchCount else 0
|
return if (nameExists) matchCount else 0
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,631 +0,0 @@
|
|||||||
package com.sukisu.ultra.ui.screen
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.DpSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.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 com.sukisu.ultra.ui.component.ImageEditorDialog
|
|
||||||
import com.sukisu.ultra.ui.component.SwitchItem
|
|
||||||
import com.sukisu.ultra.ui.theme.*
|
|
||||||
import com.sukisu.ultra.ui.util.*
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import com.sukisu.ultra.R
|
|
||||||
import com.sukisu.ultra.*
|
|
||||||
|
|
||||||
|
|
||||||
fun saveCardConfig(context: Context) {
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
|
||||||
with(prefs.edit()) {
|
|
||||||
putFloat("card_alpha", CardConfig.cardAlpha)
|
|
||||||
putBoolean("custom_background_enabled", CardConfig.cardElevation == 0.dp)
|
|
||||||
putBoolean("is_custom_alpha_set", CardConfig.isCustomAlphaSet)
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Destination<RootGraph>
|
|
||||||
@Composable
|
|
||||||
fun MoreSettingsScreen(navigator: DestinationsNavigator) {
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
|
||||||
val context = LocalContext.current
|
|
||||||
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
|
||||||
// 主题模式选择
|
|
||||||
var themeMode by remember {
|
|
||||||
mutableIntStateOf(
|
|
||||||
when(ThemeConfig.forceDarkMode) {
|
|
||||||
true -> 2 // 深色
|
|
||||||
false -> 1 // 浅色
|
|
||||||
null -> 0 // 跟随系统
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 动态颜色开关状态
|
|
||||||
var useDynamicColor by remember {
|
|
||||||
mutableStateOf(ThemeConfig.useDynamicColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
var showThemeModeDialog by remember { mutableStateOf(false) }
|
|
||||||
// 主题模式选项
|
|
||||||
val themeOptions = listOf(
|
|
||||||
stringResource(R.string.theme_follow_system),
|
|
||||||
stringResource(R.string.theme_light),
|
|
||||||
stringResource(R.string.theme_dark)
|
|
||||||
)
|
|
||||||
|
|
||||||
// 简洁模块开关状态
|
|
||||||
var isSimpleMode by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_simple_mode", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新简洁模块开关状态
|
|
||||||
val onSimpleModeChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_simple_mode", newValue) }
|
|
||||||
isSimpleMode = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核 KernelSU 版本号开关状态
|
|
||||||
var isHideVersion by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_hide_version", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏内核 KernelSU 版本号模块开关状态
|
|
||||||
val onHideVersionChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_hide_version", newValue) }
|
|
||||||
isHideVersion = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏模块数量等信息开关状态
|
|
||||||
var isHideOtherInfo by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_hide_other_info", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏模块数量等信息开关状态
|
|
||||||
val onHideOtherInfoChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_hide_other_info", newValue) }
|
|
||||||
isHideOtherInfo = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏 SuSFS 状态开关状态
|
|
||||||
var isHideSusfsStatus by remember {
|
|
||||||
mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 隐藏 SuSFS 状态开关状态
|
|
||||||
val onHideSusfsStatusChange = { newValue: Boolean ->
|
|
||||||
prefs.edit { putBoolean("is_hide_susfs_status", newValue) }
|
|
||||||
isHideSusfsStatus = newValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// SELinux 状态
|
|
||||||
var selinuxEnabled by remember {
|
|
||||||
mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 卡片配置状态
|
|
||||||
var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) }
|
|
||||||
var showCardSettings by remember { mutableStateOf(false) }
|
|
||||||
var isCustomBackgroundEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(ThemeConfig.customBackgroundUri != null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 图片编辑状态
|
|
||||||
var showImageEditor by remember { mutableStateOf(false) }
|
|
||||||
var selectedImageUri by remember { mutableStateOf<Uri?>(null) }
|
|
||||||
|
|
||||||
// 初始化卡片配置
|
|
||||||
val systemIsDark = isSystemInDarkTheme()
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
CardConfig.apply {
|
|
||||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
|
||||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
|
||||||
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
|
|
||||||
|
|
||||||
// 如果没有手动设置透明度,且是深色模式,则使用默认值
|
|
||||||
if (!isCustomAlphaSet) {
|
|
||||||
val isDarkMode = ThemeConfig.forceDarkMode ?: systemIsDark
|
|
||||||
if (isDarkMode) {
|
|
||||||
cardAlpha = 0.35f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
themeMode = when (ThemeConfig.forceDarkMode) {
|
|
||||||
true -> 2
|
|
||||||
false -> 1
|
|
||||||
null -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主题色选项
|
|
||||||
val themeColorOptions = listOf(
|
|
||||||
stringResource(R.string.color_default) to ThemeColors.Default,
|
|
||||||
stringResource(R.string.color_blue) to ThemeColors.Blue,
|
|
||||||
stringResource(R.string.color_green) to ThemeColors.Green,
|
|
||||||
stringResource(R.string.color_purple) to ThemeColors.Purple,
|
|
||||||
stringResource(R.string.color_orange) to ThemeColors.Orange,
|
|
||||||
stringResource(R.string.color_pink) to ThemeColors.Pink,
|
|
||||||
stringResource(R.string.color_gray) to ThemeColors.Gray,
|
|
||||||
stringResource(R.string.color_yellow) to ThemeColors.Yellow
|
|
||||||
)
|
|
||||||
|
|
||||||
var showThemeColorDialog by remember { mutableStateOf(false) }
|
|
||||||
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
|
|
||||||
|
|
||||||
// 图片选择器
|
|
||||||
val pickImageLauncher = rememberLauncherForActivityResult(
|
|
||||||
ActivityResultContracts.GetContent()
|
|
||||||
) { uri: Uri? ->
|
|
||||||
uri?.let {
|
|
||||||
selectedImageUri = it
|
|
||||||
showImageEditor = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示图片编辑对话框
|
|
||||||
if (showImageEditor && selectedImageUri != null) {
|
|
||||||
ImageEditorDialog(
|
|
||||||
imageUri = selectedImageUri!!,
|
|
||||||
onDismiss = {
|
|
||||||
showImageEditor = false
|
|
||||||
selectedImageUri = null
|
|
||||||
},
|
|
||||||
onConfirm = { transformedUri ->
|
|
||||||
context.saveAndApplyCustomBackground(transformedUri)
|
|
||||||
isCustomBackgroundEnabled = true
|
|
||||||
CardConfig.cardElevation = 0.dp
|
|
||||||
CardConfig.isCustomBackgroundEnabled = true
|
|
||||||
saveCardConfig(context)
|
|
||||||
showImageEditor = false
|
|
||||||
selectedImageUri = null
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 开关
|
|
||||||
if (ksuIsValid) {
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Security,
|
|
||||||
title = stringResource(R.string.selinux),
|
|
||||||
summary = if (selinuxEnabled)
|
|
||||||
stringResource(R.string.selinux_enabled) else
|
|
||||||
stringResource(R.string.selinux_disabled),
|
|
||||||
checked = selinuxEnabled
|
|
||||||
) { enabled ->
|
|
||||||
val command = if (enabled) "setenforce 1" else "setenforce 0"
|
|
||||||
Shell.getShell().newJob().add(command).exec().let { result ->
|
|
||||||
if (result.isSuccess) selinuxEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var isExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.AutoFixHigh, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.custom_settings)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
isExpanded = !isExpanded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isExpanded,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
|
||||||
) {
|
|
||||||
// 添加简洁模块开关
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.Brush,
|
|
||||||
title = stringResource(R.string.simple_mode),
|
|
||||||
summary = stringResource(R.string.simple_mode_summary),
|
|
||||||
checked = isSimpleMode
|
|
||||||
) {
|
|
||||||
onSimpleModeChange(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isExpanded,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
|
||||||
) {
|
|
||||||
// 隐藏内核部分版本号
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.VisibilityOff,
|
|
||||||
title = stringResource(R.string.hide_kernel_kernelsu_version),
|
|
||||||
summary = stringResource(R.string.hide_kernel_kernelsu_version_summary),
|
|
||||||
checked = isHideVersion
|
|
||||||
) {
|
|
||||||
onHideVersionChange(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isExpanded,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
|
||||||
) {
|
|
||||||
// 模块数量等信息
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.VisibilityOff,
|
|
||||||
title = stringResource(R.string.hide_other_info),
|
|
||||||
summary = stringResource(R.string.hide_other_info_summary),
|
|
||||||
checked = isHideOtherInfo
|
|
||||||
) {
|
|
||||||
onHideOtherInfoChange(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isExpanded,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
|
||||||
) {
|
|
||||||
// SuSFS 状态信息
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.VisibilityOff,
|
|
||||||
title = stringResource(R.string.hide_susfs_status),
|
|
||||||
summary = stringResource(R.string.hide_susfs_status_summary),
|
|
||||||
checked = isHideSusfsStatus
|
|
||||||
) {
|
|
||||||
onHideSusfsStatusChange(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// region SUSFS 配置(仅在支持时显示)
|
|
||||||
val suSFS = getSuSFS()
|
|
||||||
val isSUS_SU = getSuSFSFeatures()
|
|
||||||
if (suSFS == "Supported") {
|
|
||||||
if (isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") {
|
|
||||||
// 初始化时,默认启用
|
|
||||||
var isEnabled by rememberSaveable {
|
|
||||||
mutableStateOf(true) // 默认启用
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在启动时检查状态
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
// 如果当前模式不是2就强制启用
|
|
||||||
val currentMode = susfsSUS_SU_Mode()
|
|
||||||
val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true)
|
|
||||||
if (currentMode != "2" && wasManuallyDisabled) {
|
|
||||||
susfsSUS_SU_2() // 强制切换到模式2
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
|
||||||
}
|
|
||||||
isEnabled = currentMode == "2"
|
|
||||||
}
|
|
||||||
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.VisibilityOff,
|
|
||||||
title = stringResource(id = R.string.settings_susfs_toggle),
|
|
||||||
summary = stringResource(id = R.string.settings_susfs_toggle_summary),
|
|
||||||
checked = isEnabled
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
// 手动启用
|
|
||||||
susfsSUS_SU_2()
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", true) }
|
|
||||||
} else {
|
|
||||||
// 手动关闭
|
|
||||||
susfsSUS_SU_0()
|
|
||||||
prefs.edit { putBoolean("enable_sus_su", false) }
|
|
||||||
}
|
|
||||||
isEnabled = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
// 动态颜色开关
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
SwitchItem(
|
|
||||||
icon = Icons.Filled.ColorLens,
|
|
||||||
title = stringResource(R.string.dynamic_color_title),
|
|
||||||
summary = stringResource(R.string.dynamic_color_summary),
|
|
||||||
checked = useDynamicColor
|
|
||||||
) { enabled ->
|
|
||||||
useDynamicColor = enabled
|
|
||||||
context.saveDynamicColorState(enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 只在未启用动态颜色时显示主题色选择
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = !useDynamicColor
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Default.Palette, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.theme_color)) },
|
|
||||||
supportingContent = {
|
|
||||||
val currentThemeName = when (ThemeConfig.currentTheme) {
|
|
||||||
is ThemeColors.Default -> stringResource(R.string.color_default)
|
|
||||||
is ThemeColors.Blue -> stringResource(R.string.color_blue)
|
|
||||||
is ThemeColors.Green -> stringResource(R.string.color_green)
|
|
||||||
is ThemeColors.Purple -> stringResource(R.string.color_purple)
|
|
||||||
is ThemeColors.Orange -> stringResource(R.string.color_orange)
|
|
||||||
is ThemeColors.Pink -> stringResource(R.string.color_pink)
|
|
||||||
is ThemeColors.Gray -> stringResource(R.string.color_gray)
|
|
||||||
is ThemeColors.Yellow -> stringResource(R.string.color_yellow)
|
|
||||||
else -> stringResource(R.string.color_default)
|
|
||||||
}
|
|
||||||
Text(currentThemeName)
|
|
||||||
},
|
|
||||||
modifier = Modifier.clickable { showThemeColorDialog = true }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (showThemeColorDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showThemeColorDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.choose_theme_color)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
themeColorOptions.forEach { (name, theme) ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
context.saveThemeColors(when (theme) {
|
|
||||||
ThemeColors.Default -> "default"
|
|
||||||
ThemeColors.Blue -> "blue"
|
|
||||||
ThemeColors.Green -> "green"
|
|
||||||
ThemeColors.Purple -> "purple"
|
|
||||||
ThemeColors.Orange -> "orange"
|
|
||||||
ThemeColors.Pink -> "pink"
|
|
||||||
ThemeColors.Gray -> "gray"
|
|
||||||
ThemeColors.Yellow -> "yellow"
|
|
||||||
else -> "default"
|
|
||||||
})
|
|
||||||
showThemeColorDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = ThemeConfig.currentTheme::class == theme::class,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
.background(theme.Primary, shape = CircleShape)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 自定义背景开关
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Wallpaper, null) },
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) },
|
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
if (isCustomBackgroundEnabled) {
|
|
||||||
showCardSettings = !showCardSettings
|
|
||||||
}
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Switch(
|
|
||||||
checked = isCustomBackgroundEnabled,
|
|
||||||
onCheckedChange = { isChecked ->
|
|
||||||
if (isChecked) {
|
|
||||||
pickImageLauncher.launch("image/*")
|
|
||||||
} else {
|
|
||||||
context.saveCustomBackground(null)
|
|
||||||
isCustomBackgroundEnabled = false
|
|
||||||
CardConfig.cardElevation = CardConfig.defaultElevation
|
|
||||||
CardConfig.cardAlpha = 0.45f
|
|
||||||
CardConfig.isCustomAlphaSet = false
|
|
||||||
CardConfig.isCustomBackgroundEnabled = false
|
|
||||||
saveCardConfig(context)
|
|
||||||
cardAlpha = 0.35f
|
|
||||||
themeMode = 0
|
|
||||||
context.saveThemeMode(null)
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// 透明度 Slider
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
|
||||||
) {
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.Opacity, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.settings_card_alpha)) },
|
|
||||||
supportingContent = {
|
|
||||||
Slider(
|
|
||||||
value = cardAlpha,
|
|
||||||
onValueChange = { newValue ->
|
|
||||||
cardAlpha = newValue
|
|
||||||
CardConfig.cardAlpha = newValue
|
|
||||||
CardConfig.isCustomAlphaSet = true
|
|
||||||
prefs.edit { putBoolean("is_custom_alpha_set", true) }
|
|
||||||
prefs.edit { putFloat("card_alpha", newValue) }
|
|
||||||
},
|
|
||||||
onValueChangeFinished = {
|
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
|
||||||
saveCardConfig(context)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
valueRange = 0f..1f,
|
|
||||||
colors = getSliderColors(cardAlpha, useCustomColors = true),
|
|
||||||
thumb = {
|
|
||||||
SliderDefaults.Thumb(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
thumbSize = DpSize(0.dp, 0.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = ThemeConfig.customBackgroundUri != null && showCardSettings,
|
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 24.dp)
|
|
||||||
){
|
|
||||||
ListItem(
|
|
||||||
leadingContent = { Icon(Icons.Filled.DarkMode, null) },
|
|
||||||
headlineContent = { Text(stringResource(R.string.theme_mode)) },
|
|
||||||
supportingContent = { Text(themeOptions[themeMode]) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
showThemeModeDialog = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主题模式选择对话框
|
|
||||||
if (showThemeModeDialog) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = { showThemeModeDialog = false },
|
|
||||||
title = { Text(stringResource(R.string.theme_mode)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
themeOptions.forEachIndexed { index, option ->
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
themeMode = index
|
|
||||||
val newThemeMode = when(index) {
|
|
||||||
0 -> null // 跟随系统
|
|
||||||
1 -> false // 浅色
|
|
||||||
2 -> true // 深色
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
context.saveThemeMode(newThemeMode)
|
|
||||||
when (index) {
|
|
||||||
2 -> {
|
|
||||||
ThemeConfig.forceDarkMode = true
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.isUserDarkModeEnabled = true
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
ThemeConfig.forceDarkMode = false
|
|
||||||
CardConfig.isUserLightModeEnabled = true
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
0 -> {
|
|
||||||
ThemeConfig.forceDarkMode = null
|
|
||||||
CardConfig.isUserLightModeEnabled = false
|
|
||||||
CardConfig.isUserDarkModeEnabled = false
|
|
||||||
CardConfig.save(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showThemeModeDialog = false
|
|
||||||
}
|
|
||||||
.padding(vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
RadioButton(
|
|
||||||
selected = themeMode == index,
|
|
||||||
onClick = null
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
|
||||||
Text(option)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun getSliderColors(cardAlpha: Float, useCustomColors: Boolean = false): SliderColors {
|
|
||||||
val theme = ThemeConfig.currentTheme
|
|
||||||
val isDarkTheme = ThemeConfig.forceDarkMode ?: isSystemInDarkTheme()
|
|
||||||
val useDynamicColor = ThemeConfig.useDynamicColor
|
|
||||||
|
|
||||||
return when {
|
|
||||||
// 使用动态颜色时
|
|
||||||
useDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
|
|
||||||
inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
|
|
||||||
thumbColor = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
// 使用自定义主题色时
|
|
||||||
useCustomColors -> {
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = theme.getCustomSliderActiveColor(),
|
|
||||||
inactiveTrackColor = theme.getCustomSliderInactiveColor(),
|
|
||||||
thumbColor = theme.Primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val activeColor = if (isDarkTheme) {
|
|
||||||
theme.Primary.copy(alpha = cardAlpha)
|
|
||||||
} else {
|
|
||||||
theme.Primary.copy(alpha = cardAlpha)
|
|
||||||
}
|
|
||||||
val inactiveColor = if (isDarkTheme) {
|
|
||||||
Color.DarkGray.copy(alpha = 0.3f)
|
|
||||||
} else {
|
|
||||||
Color.LightGray.copy(alpha = 0.3f)
|
|
||||||
}
|
|
||||||
SliderDefaults.colors(
|
|
||||||
activeTrackColor = activeColor,
|
|
||||||
inactiveTrackColor = inactiveColor,
|
|
||||||
thumbColor = activeColor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,20 @@ import android.net.Uri
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
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.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Undo
|
import androidx.compose.material.icons.automirrored.filled.Undo
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -24,13 +30,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
|||||||
import androidx.compose.runtime.setValue
|
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.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
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.style.LineHeightStyle
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
@@ -42,41 +47,47 @@ import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplat
|
|||||||
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
|
||||||
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
|
||||||
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
|
||||||
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 com.sukisu.ultra.BuildConfig
|
import com.sukisu.ultra.BuildConfig
|
||||||
import com.sukisu.ultra.Natives
|
import com.sukisu.ultra.Natives
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.*
|
|
||||||
import com.sukisu.ultra.ui.component.*
|
import com.sukisu.ultra.ui.component.*
|
||||||
import com.sukisu.ultra.ui.theme.*
|
import com.sukisu.ultra.ui.theme.*
|
||||||
|
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
|
||||||
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
import com.sukisu.ultra.ui.util.LocalSnackbarHost
|
||||||
import com.sukisu.ultra.ui.util.getBugreportFile
|
import com.sukisu.ultra.ui.util.getBugreportFile
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
import com.sukisu.ultra.ui.component.KsuIsValid
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author weishu
|
* @author ShirkNeko
|
||||||
* @date 2023/1/1.
|
* @date 2025/5/31.
|
||||||
*/
|
*/
|
||||||
|
private val SPACING_SMALL = 3.dp
|
||||||
|
private val SPACING_MEDIUM = 8.dp
|
||||||
|
private val SPACING_LARGE = 16.dp
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Destination<RootGraph>
|
@Destination<RootGraph>
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingScreen(navigator: DestinationsNavigator) {
|
fun SettingScreen(navigator: DestinationsNavigator) {
|
||||||
// region 界面基础设置
|
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
|
||||||
val snackBarHost = LocalSnackbarHost.current
|
val snackBarHost = LocalSnackbarHost.current
|
||||||
val ksuIsValid = Natives.isKsuValid(ksuApp.packageName)
|
val context = LocalContext.current
|
||||||
// endregion
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
var selectedEngine by rememberSaveable {
|
||||||
|
mutableStateOf(
|
||||||
|
prefs.getString("webui_engine", "default") ?: "default"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
// containerColor = MaterialTheme.colorScheme.surfaceBright,
|
||||||
topBar = {
|
topBar = {
|
||||||
TopBar(
|
TopBar(scrollBehavior = scrollBehavior)
|
||||||
scrollBehavior = scrollBehavior
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
snackbarHost = { SnackbarHost(snackBarHost) },
|
snackbarHost = { SnackbarHost(snackBarHost) },
|
||||||
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
|
||||||
@@ -85,7 +96,6 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
AboutDialog(it)
|
AboutDialog(it)
|
||||||
}
|
}
|
||||||
val loadingDialog = rememberLoadingDialog()
|
val loadingDialog = rememberLoadingDialog()
|
||||||
// endregion
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -93,12 +103,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
// region 上下文与协程
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// endregion
|
|
||||||
|
|
||||||
// region 日志导出功能
|
|
||||||
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
val exportBugreportLauncher = rememberLauncherForActivityResult(
|
||||||
ActivityResultContracts.CreateDocument("application/gzip")
|
ActivityResultContracts.CreateDocument("application/gzip")
|
||||||
) { uri: Uri? ->
|
) { uri: Uri? ->
|
||||||
@@ -114,165 +120,179 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// region 配置项列表
|
|
||||||
|
// 配置卡片
|
||||||
|
KsuIsValid {
|
||||||
|
SettingsGroupCard(
|
||||||
|
title = stringResource(R.string.configuration),
|
||||||
|
content = {
|
||||||
// 配置文件模板入口
|
// 配置文件模板入口
|
||||||
val profileTemplate = stringResource(id = R.string.settings_profile_template)
|
SettingItem(
|
||||||
if (ksuIsValid) {
|
icon = Icons.Filled.Fence,
|
||||||
ListItem(
|
title = stringResource(R.string.settings_profile_template),
|
||||||
leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },
|
summary = stringResource(R.string.settings_profile_template_summary),
|
||||||
headlineContent = { Text(profileTemplate) },
|
onClick = {
|
||||||
supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
navigator.navigate(AppProfileTemplateScreenDestination)
|
navigator.navigate(AppProfileTemplateScreenDestination)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
// 卸载模块开关
|
// 卸载模块开关
|
||||||
var umountChecked by rememberSaveable {
|
var umountChecked by rememberSaveable {
|
||||||
mutableStateOf(Natives.isDefaultUmountModules())
|
mutableStateOf(Natives.isDefaultUmountModules())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ksuIsValid) {
|
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.FolderDelete,
|
icon = Icons.Filled.FolderDelete,
|
||||||
title = stringResource(id = R.string.settings_umount_modules_default),
|
title = stringResource(R.string.settings_umount_modules_default),
|
||||||
summary = stringResource(id = R.string.settings_umount_modules_default_summary),
|
summary = stringResource(R.string.settings_umount_modules_default_summary),
|
||||||
checked = umountChecked
|
checked = umountChecked,
|
||||||
) {
|
onCheckedChange = { enabled ->
|
||||||
if (Natives.setDefaultUmountModules(it)) {
|
if (Natives.setDefaultUmountModules(enabled)) {
|
||||||
umountChecked = it
|
umountChecked = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
// SU 禁用开关(仅在兼容版本显示)
|
|
||||||
if (ksuIsValid) {
|
// SU 禁用开关
|
||||||
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
|
||||||
var isSuDisabled by rememberSaveable {
|
var isSuDisabled by rememberSaveable {
|
||||||
mutableStateOf(!Natives.isSuEnabled())
|
mutableStateOf(!Natives.isSuEnabled())
|
||||||
}
|
}
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.RemoveModerator,
|
icon = Icons.Filled.RemoveModerator,
|
||||||
title = stringResource(id = R.string.settings_disable_su),
|
title = stringResource(R.string.settings_disable_su),
|
||||||
summary = stringResource(id = R.string.settings_disable_su_summary),
|
summary = stringResource(R.string.settings_disable_su_summary),
|
||||||
checked = isSuDisabled,
|
checked = isSuDisabled,
|
||||||
) { checked ->
|
onCheckedChange = { enabled ->
|
||||||
val shouldEnable = !checked
|
val shouldEnable = !enabled
|
||||||
if (Natives.setSuEnabled(shouldEnable)) {
|
if (Natives.setSuEnabled(shouldEnable)) {
|
||||||
isSuDisabled = !shouldEnable
|
isSuDisabled = enabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
// 强制签名验证开关
|
||||||
|
var forceSignatureVerification by rememberSaveable {
|
||||||
|
mutableStateOf(prefs.getBoolean("force_signature_verification", false))
|
||||||
|
}
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.Security,
|
||||||
|
title = stringResource(R.string.module_signature_verification),
|
||||||
|
summary = stringResource(R.string.module_signature_verification_summary),
|
||||||
|
checked = forceSignatureVerification,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
|
prefs.edit { putBoolean("force_signature_verification", enabled) }
|
||||||
|
forceSignatureVerification = enabled
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
// 应用设置卡片
|
||||||
|
SettingsGroupCard(
|
||||||
|
title = stringResource(R.string.app_settings),
|
||||||
|
content = {
|
||||||
// 更新检查开关
|
// 更新检查开关
|
||||||
var checkUpdate by rememberSaveable {
|
var checkUpdate by rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(prefs.getBoolean("check_update", true))
|
||||||
prefs.getBoolean("check_update", true)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.Update,
|
icon = Icons.Filled.Update,
|
||||||
title = stringResource(id = R.string.settings_check_update),
|
title = stringResource(R.string.settings_check_update),
|
||||||
summary = stringResource(id = R.string.settings_check_update_summary),
|
summary = stringResource(R.string.settings_check_update_summary),
|
||||||
checked = checkUpdate
|
checked = checkUpdate,
|
||||||
) {
|
onCheckedChange = { enabled ->
|
||||||
prefs.edit {putBoolean("check_update", it) }
|
prefs.edit { putBoolean("check_update", enabled) }
|
||||||
checkUpdate = it
|
checkUpdate = enabled
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebUI引擎选择
|
||||||
|
KsuIsValid {
|
||||||
|
WebUIEngineSelector(
|
||||||
|
selectedEngine = selectedEngine,
|
||||||
|
onEngineSelected = { engine ->
|
||||||
|
selectedEngine = engine
|
||||||
|
prefs.edit { putString("webui_engine", engine) }
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Web调试开关
|
// Web调试和Web X Eruda 开关
|
||||||
var enableWebDebugging by rememberSaveable {
|
var enableWebDebugging by rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(prefs.getBoolean("enable_web_debugging", false))
|
||||||
prefs.getBoolean("enable_web_debugging", false)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (Natives.isKsuValid(ksuApp.packageName)) {
|
var useWebUIXEruda by rememberSaveable {
|
||||||
|
mutableStateOf(prefs.getBoolean("use_webuix_eruda", false))
|
||||||
|
}
|
||||||
|
|
||||||
|
KsuIsValid {
|
||||||
SwitchItem(
|
SwitchItem(
|
||||||
icon = Icons.Filled.DeveloperMode,
|
icon = Icons.Filled.DeveloperMode,
|
||||||
title = stringResource(id = R.string.enable_web_debugging),
|
title = stringResource(R.string.enable_web_debugging),
|
||||||
summary = stringResource(id = R.string.enable_web_debugging_summary),
|
summary = stringResource(R.string.enable_web_debugging_summary),
|
||||||
checked = enableWebDebugging
|
checked = enableWebDebugging,
|
||||||
) {
|
onCheckedChange = { enabled ->
|
||||||
prefs.edit { putBoolean("enable_web_debugging", it) }
|
prefs.edit { putBoolean("enable_web_debugging", enabled) }
|
||||||
enableWebDebugging = it
|
enableWebDebugging = enabled
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// 更多设置
|
|
||||||
val newButtonTitle = stringResource(id = R.string.more_settings)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Settings,
|
|
||||||
contentDescription = newButtonTitle
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
headlineContent = { Text(newButtonTitle) },
|
AnimatedVisibility(
|
||||||
supportingContent = { Text(stringResource(id = R.string.more_settings)) },
|
visible = enableWebDebugging && selectedEngine == "wx",
|
||||||
modifier = Modifier.clickable {
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = fadeOut() + shrinkVertically()
|
||||||
|
) {
|
||||||
|
SwitchItem(
|
||||||
|
icon = Icons.Filled.FormatListNumbered,
|
||||||
|
title = stringResource(R.string.use_webuix_eruda),
|
||||||
|
summary = stringResource(R.string.use_webuix_eruda_summary),
|
||||||
|
checked = useWebUIXEruda,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
|
prefs.edit { putBoolean("use_webuix_eruda", enabled) }
|
||||||
|
useWebUIXEruda = enabled
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更多设置
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Settings,
|
||||||
|
title = stringResource(R.string.more_settings),
|
||||||
|
summary = stringResource(R.string.more_settings),
|
||||||
|
onClick = {
|
||||||
navigator.navigate(MoreSettingsScreenDestination)
|
navigator.navigate(MoreSettingsScreenDestination)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 工具卡片
|
||||||
|
SettingsGroupCard(
|
||||||
|
title = stringResource(R.string.tools),
|
||||||
|
content = {
|
||||||
var showBottomsheet by remember { mutableStateOf(false) }
|
var showBottomsheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
ListItem(
|
SettingItem(
|
||||||
leadingContent = {
|
icon = Icons.Filled.BugReport,
|
||||||
Icon(
|
title = stringResource(R.string.send_log),
|
||||||
Icons.Filled.BugReport,
|
onClick = {
|
||||||
stringResource(id = R.string.send_log)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(stringResource(id = R.string.send_log)) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
showBottomsheet = true
|
showBottomsheet = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (showBottomsheet) {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = { showBottomsheet = false },
|
|
||||||
content = {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(10.dp)
|
|
||||||
.align(Alignment.CenterHorizontally)
|
|
||||||
|
|
||||||
) {
|
if (showBottomsheet) {
|
||||||
Box {
|
LogBottomSheet(
|
||||||
Column(
|
onDismiss = { showBottomsheet = false },
|
||||||
modifier = Modifier
|
onSaveLog = {
|
||||||
.padding(16.dp)
|
|
||||||
.clickable {
|
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
|
||||||
val current = LocalDateTime.now().format(formatter)
|
val current = LocalDateTime.now().format(formatter)
|
||||||
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
|
||||||
showBottomsheet = false
|
showBottomsheet = false
|
||||||
}
|
},
|
||||||
) {
|
onShareLog = {
|
||||||
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 {
|
scope.launch {
|
||||||
val bugreport = loadingDialog.withLoading {
|
val bugreport = loadingDialog.withLoading {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
@@ -280,8 +300,7 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val uri: Uri =
|
val uri = FileProvider.getUriForFile(
|
||||||
FileProvider.getUriForFile(
|
|
||||||
context,
|
context,
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
||||||
bugreport
|
bugreport
|
||||||
@@ -299,26 +318,8 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
context.getString(R.string.send_log)
|
context.getString(R.string.send_log)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
showBottomsheet = false
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.Share,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = R.string.send_log),
|
|
||||||
modifier = Modifier.padding(top = 16.dp),
|
|
||||||
textAlign = TextAlign.Center.also {
|
|
||||||
LineHeightStyle(
|
|
||||||
alignment = LineHeightStyle.Alignment.Center,
|
|
||||||
trim = LineHeightStyle.Trim.None
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -330,21 +331,263 @@ fun SettingScreen(navigator: DestinationsNavigator) {
|
|||||||
loadingDialog.withLoading(it)
|
loadingDialog.withLoading(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
val about = stringResource(id = R.string.about)
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
Icons.Filled.ContactPage,
|
|
||||||
about
|
|
||||||
)
|
)
|
||||||
},
|
|
||||||
headlineContent = { Text(about) },
|
// 关于卡片
|
||||||
modifier = Modifier.clickable {
|
SettingsGroupCard(
|
||||||
|
title = stringResource(R.string.about),
|
||||||
|
content = {
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.Info,
|
||||||
|
title = stringResource(R.string.about),
|
||||||
|
onClick = {
|
||||||
aboutDialog.show()
|
aboutDialog.show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsGroupCard(
|
||||||
|
title: String,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||||
|
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||||
|
elevation = getCardElevation()
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(vertical = SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WebUIEngineSelector(
|
||||||
|
selectedEngine: String,
|
||||||
|
onEngineSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
val engineOptions = listOf(
|
||||||
|
"default" to stringResource(R.string.engine_auto_select),
|
||||||
|
"wx" to stringResource(R.string.engine_force_webuix),
|
||||||
|
"ksu" to stringResource(R.string.engine_force_ksu)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
icon = Icons.Filled.WebAsset,
|
||||||
|
title = stringResource(R.string.use_webuix),
|
||||||
|
summary = engineOptions.find { it.first == selectedEngine }?.second
|
||||||
|
?: stringResource(R.string.engine_auto_select),
|
||||||
|
onClick = { showDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.use_webuix)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
engineOptions.forEach { (value, label) ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
onEngineSelected(value)
|
||||||
|
showDialog = false
|
||||||
|
}
|
||||||
|
.padding(vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = selectedEngine == value,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
|
||||||
|
Text(text = label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { showDialog = false }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun LogBottomSheet(
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onSaveLog: () -> Unit,
|
||||||
|
onShareLog: () -> Unit
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(SPACING_LARGE),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
|
) {
|
||||||
|
LogActionButton(
|
||||||
|
icon = Icons.Filled.Save,
|
||||||
|
text = stringResource(R.string.save_log),
|
||||||
|
onClick = onSaveLog
|
||||||
|
)
|
||||||
|
|
||||||
|
LogActionButton(
|
||||||
|
icon = Icons.Filled.Share,
|
||||||
|
text = stringResource(R.string.send_log),
|
||||||
|
onClick = onShareLog
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_LARGE))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LogActionButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(SPACING_MEDIUM)
|
||||||
|
) {
|
||||||
|
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(SPACING_MEDIUM))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = SPACING_LARGE)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
if (summary != null) {
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
summary: String? = null,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onCheckedChange(!checked) }
|
||||||
|
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.Top
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = SPACING_LARGE)
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
if (summary != null) {
|
||||||
|
Spacer(modifier = Modifier.height(SPACING_SMALL))
|
||||||
|
Text(
|
||||||
|
text = summary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,16 +624,11 @@ fun UninstallItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val uninstall = stringResource(id = R.string.settings_uninstall)
|
|
||||||
ListItem(
|
SettingItem(
|
||||||
leadingContent = {
|
icon = Icons.Filled.Delete,
|
||||||
Icon(
|
title = stringResource(id = R.string.settings_uninstall),
|
||||||
Icons.Filled.Delete,
|
onClick = {
|
||||||
uninstall
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(uninstall) },
|
|
||||||
modifier = Modifier.clickable {
|
|
||||||
uninstallDialog.show()
|
uninstallDialog.show()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -432,75 +670,124 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selection = UninstallType.NONE
|
var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
|
||||||
val cardColor = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = MaterialTheme.colorScheme.copy(
|
||||||
|
surface = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
)
|
||||||
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
dismiss()
|
dismiss()
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
Text(text = stringResource(R.string.settings_uninstall))
|
Text(
|
||||||
|
text = stringResource(R.string.settings_uninstall),
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Column {
|
Column(
|
||||||
listOptions.forEachIndexed { index, option ->
|
modifier = Modifier.padding(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
options.forEachIndexed { index, option ->
|
||||||
|
val isSelected = selectedOption == option
|
||||||
|
val backgroundColor = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
else
|
||||||
|
Color.Transparent
|
||||||
|
val contentColor = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.background(backgroundColor)
|
||||||
.clickable {
|
.clickable {
|
||||||
selection = options[index]
|
selectedOption = option
|
||||||
}
|
}
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 12.dp, horizontal = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = options[index].icon,
|
imageVector = option.icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp)
|
||||||
)
|
)
|
||||||
Column {
|
Column(
|
||||||
Text(text = option.titleText)
|
modifier = Modifier.weight(1f)
|
||||||
option.subtitleText?.let {
|
) {
|
||||||
|
Text(
|
||||||
|
text = listOptions[index].titleText,
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
listOptions[index].subtitleText?.let {
|
||||||
Text(
|
Text(
|
||||||
text = it,
|
text = it,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = if (isSelected)
|
||||||
|
contentColor.copy(alpha = 0.8f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RadioButtonChecked,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.RadioButtonUnchecked,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
androidx.compose.material3.TextButton(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (selection != UninstallType.NONE) {
|
selectedOption?.let { onSelected(it) }
|
||||||
onSelected(selection)
|
|
||||||
}
|
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
},
|
||||||
|
enabled = selectedOption != null,
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.ok))
|
Text(
|
||||||
|
text = stringResource(android.R.string.ok)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
androidx.compose.material3.TextButton(
|
TextButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(text = stringResource(android.R.string.cancel))
|
Text(
|
||||||
|
text = stringResource(android.R.string.cancel),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
containerColor = getCardColors(cardColor.copy(alpha = 0.9f)).containerColor.copy(alpha = 0.9f),
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
shape = MaterialTheme.shapes.medium,
|
tonalElevation = 4.dp
|
||||||
tonalElevation = getCardElevation()
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -508,24 +795,24 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
|
|||||||
private fun TopBar(
|
private fun TopBar(
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
val cardColor = MaterialTheme.colorScheme.secondaryContainer
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
val cardAlpha = CardConfig.cardAlpha
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
|
colorScheme.surfaceContainerLow
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(stringResource(R.string.settings)) },
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = cardColor.copy(alpha = cardAlpha),
|
containerColor = cardColor.copy(alpha = cardAlpha),
|
||||||
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
|
||||||
),
|
),
|
||||||
|
|
||||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||||
scrollBehavior = scrollBehavior
|
scrollBehavior = scrollBehavior
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
|
||||||
@Composable
|
|
||||||
private fun SettingsPreview() {
|
|
||||||
SettingScreen(EmptyDestinationsNavigator)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
2110
manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt
Normal file
2110
manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -33,6 +33,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
|
||||||
@@ -61,7 +62,7 @@ import com.ramcosta.composedestinations.result.getOr
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import com.sukisu.ultra.R
|
import com.sukisu.ultra.R
|
||||||
import com.sukisu.ultra.ui.theme.ThemeConfig
|
import com.sukisu.ultra.ui.theme.CardConfig
|
||||||
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
import com.sukisu.ultra.ui.viewmodel.TemplateViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,11 +80,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 = if (!ThemeConfig.useDynamicColor) {
|
|
||||||
ThemeConfig.currentTheme.ButtonContrast
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (viewModel.templateList.isEmpty()) {
|
if (viewModel.templateList.isEmpty()) {
|
||||||
@@ -98,6 +94,9 @@ fun AppProfileTemplateScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cardColorUse = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
val cardAlpha = CardConfig.cardAlpha
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -109,6 +108,10 @@ 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) }
|
||||||
},
|
},
|
||||||
@@ -155,7 +158,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
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -207,7 +209,7 @@ private fun TemplateItem(
|
|||||||
FlowRow {
|
FlowRow {
|
||||||
LabelText(label = "UID: ${template.uid}")
|
LabelText(label = "UID: ${template.uid}")
|
||||||
LabelText(label = "GID: ${template.gid}")
|
LabelText(label = "GID: ${template.gid}")
|
||||||
LabelText(label = template.context)
|
LabelText(label = template.context,)
|
||||||
if (template.local) {
|
if (template.local) {
|
||||||
LabelText(label = "local")
|
LabelText(label = "local")
|
||||||
} else {
|
} else {
|
||||||
@@ -215,7 +217,7 @@ private fun TemplateItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,12 +228,25 @@ private fun TopBar(
|
|||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
onImport: () -> Unit = {},
|
onImport: () -> Unit = {},
|
||||||
onExport: () -> Unit = {},
|
onExport: () -> Unit = {},
|
||||||
|
colors: TopAppBarColors,
|
||||||
scrollBehavior: TopAppBarScrollBehavior? = null
|
scrollBehavior: TopAppBarScrollBehavior? = null
|
||||||
) {
|
) {
|
||||||
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
|
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
|
||||||
|
colorScheme.surfaceContainerLow
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|||||||
@@ -5,84 +5,119 @@ import androidx.compose.foundation.isSystemInDarkTheme
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
object CardConfig {
|
object CardConfig {
|
||||||
val defaultElevation: Dp = 0.dp
|
// 卡片透明度
|
||||||
|
var cardAlpha by mutableFloatStateOf(1f)
|
||||||
var cardAlpha by mutableStateOf(0.45f)
|
// 卡片亮度
|
||||||
var cardElevation by mutableStateOf(defaultElevation)
|
var cardDim by mutableFloatStateOf(0f)
|
||||||
|
// 卡片阴影
|
||||||
|
var cardElevation by mutableStateOf(0.dp)
|
||||||
var isShadowEnabled by mutableStateOf(true)
|
var isShadowEnabled by mutableStateOf(true)
|
||||||
var isCustomAlphaSet by mutableStateOf(false)
|
var isCustomAlphaSet by mutableStateOf(false)
|
||||||
|
var isCustomDimSet by mutableStateOf(false)
|
||||||
var isUserDarkModeEnabled by mutableStateOf(false)
|
var isUserDarkModeEnabled by mutableStateOf(false)
|
||||||
var isUserLightModeEnabled by mutableStateOf(false)
|
var isUserLightModeEnabled by mutableStateOf(false)
|
||||||
var isCustomBackgroundEnabled by mutableStateOf(false)
|
var isCustomBackgroundEnabled by mutableStateOf(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存卡片配置到SharedPreferences
|
||||||
|
*/
|
||||||
fun save(context: Context) {
|
fun save(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
prefs.edit().apply {
|
prefs.edit().apply {
|
||||||
putFloat("card_alpha", cardAlpha)
|
putFloat("card_alpha", cardAlpha)
|
||||||
putBoolean("custom_background_enabled", cardElevation == 0.dp)
|
putFloat("card_dim", cardDim)
|
||||||
|
putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
|
||||||
|
putBoolean("is_shadow_enabled", isShadowEnabled)
|
||||||
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
putBoolean("is_custom_alpha_set", isCustomAlphaSet)
|
||||||
|
putBoolean("is_custom_dim_set", isCustomDimSet)
|
||||||
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
|
||||||
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
|
||||||
putBoolean("is_custom_background_enabled", isCustomBackgroundEnabled)
|
|
||||||
apply()
|
apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从SharedPreferences加载卡片配置
|
||||||
|
*/
|
||||||
fun load(context: Context) {
|
fun load(context: Context) {
|
||||||
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
cardAlpha = prefs.getFloat("card_alpha", 0.45f)
|
cardAlpha = prefs.getFloat("card_alpha", 1f)
|
||||||
cardElevation = if (prefs.getBoolean("custom_background_enabled", false)) 0.dp else defaultElevation
|
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)
|
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)
|
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
|
||||||
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
|
||||||
isCustomBackgroundEnabled = prefs.getBoolean("is_custom_background_enabled", false)
|
updateShadowEnabled(isShadowEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新阴影启用状态
|
||||||
|
*/
|
||||||
fun updateShadowEnabled(enabled: Boolean) {
|
fun updateShadowEnabled(enabled: Boolean) {
|
||||||
isShadowEnabled = enabled
|
isShadowEnabled = enabled
|
||||||
cardElevation = if (enabled) defaultElevation else 0.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setDarkModeDefaults() {
|
|
||||||
if (!isCustomAlphaSet) {
|
|
||||||
cardAlpha = 0.35f
|
|
||||||
cardElevation = 0.dp
|
cardElevation = 0.dp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置主题模式默认值
|
||||||
|
*/
|
||||||
|
fun setThemeDefaults(isDarkMode: Boolean) {
|
||||||
|
if (!isCustomAlphaSet) {
|
||||||
|
cardAlpha = 1f
|
||||||
|
}
|
||||||
|
if (!isCustomDimSet) {
|
||||||
|
cardDim = if (isDarkMode) 0.5f else 0f
|
||||||
|
}
|
||||||
|
updateShadowEnabled(isShadowEnabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取卡片颜色配置
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun getCardColors(originalColor: Color) = CardDefaults.elevatedCardColors(
|
fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
|
||||||
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
containerColor = originalColor.copy(alpha = CardConfig.cardAlpha),
|
||||||
contentColor = when {
|
contentColor = determineContentColor(originalColor)
|
||||||
CardConfig.isUserLightModeEnabled -> {
|
|
||||||
Color.Black
|
|
||||||
}
|
|
||||||
CardConfig.isUserDarkModeEnabled -> {
|
|
||||||
Color.White
|
|
||||||
}
|
|
||||||
!isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled -> {
|
|
||||||
Color.Black
|
|
||||||
}
|
|
||||||
!isSystemInDarkTheme() && !CardConfig.isCustomBackgroundEnabled && !CardConfig.isUserDarkModeEnabled && originalColor.luminance() > 0.3 -> {
|
|
||||||
Color.Black
|
|
||||||
}
|
|
||||||
isSystemInDarkTheme() && !CardConfig.isUserDarkModeEnabled && !CardConfig.isUserLightModeEnabled-> {
|
|
||||||
Color.White
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
Color.White
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getCardElevation() = CardConfig.cardElevation
|
/**
|
||||||
|
* 获取卡片阴影配置
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun getCardElevation() = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = CardConfig.cardElevation,
|
||||||
|
pressedElevation = CardConfig.cardElevation,
|
||||||
|
focusedElevation = CardConfig.cardElevation,
|
||||||
|
hoveredElevation = CardConfig.cardElevation,
|
||||||
|
draggedElevation = CardConfig.cardElevation,
|
||||||
|
disabledElevation = CardConfig.cardElevation
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据背景颜色、主题模式和用户设置确定内容颜色
|
||||||
|
*/
|
||||||
|
@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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,167 +3,612 @@ package com.sukisu.ultra.ui.theme
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
sealed class ThemeColors {
|
sealed class ThemeColors {
|
||||||
abstract val Primary: Color
|
// 浅色
|
||||||
abstract val Secondary: Color
|
abstract val primaryLight: Color
|
||||||
abstract val Tertiary: Color
|
abstract val onPrimaryLight: Color
|
||||||
abstract val OnPrimary: Color
|
abstract val primaryContainerLight: Color
|
||||||
abstract val OnSecondary: Color
|
abstract val onPrimaryContainerLight: Color
|
||||||
abstract val OnTertiary: Color
|
abstract val secondaryLight: Color
|
||||||
abstract val PrimaryContainer: Color
|
abstract val onSecondaryLight: Color
|
||||||
abstract val SecondaryContainer: Color
|
abstract val secondaryContainerLight: Color
|
||||||
abstract val TertiaryContainer: Color
|
abstract val onSecondaryContainerLight: Color
|
||||||
abstract val OnPrimaryContainer: Color
|
abstract val tertiaryLight: Color
|
||||||
abstract val OnSecondaryContainer: Color
|
abstract val onTertiaryLight: Color
|
||||||
abstract val OnTertiaryContainer: Color
|
abstract val tertiaryContainerLight: Color
|
||||||
abstract val ButtonContrast: Color
|
abstract val onTertiaryContainerLight: Color
|
||||||
|
abstract val errorLight: Color
|
||||||
|
abstract val onErrorLight: Color
|
||||||
|
abstract val errorContainerLight: Color
|
||||||
|
abstract val onErrorContainerLight: Color
|
||||||
|
abstract val backgroundLight: Color
|
||||||
|
abstract val onBackgroundLight: Color
|
||||||
|
abstract val surfaceLight: Color
|
||||||
|
abstract val onSurfaceLight: Color
|
||||||
|
abstract val surfaceVariantLight: Color
|
||||||
|
abstract val onSurfaceVariantLight: Color
|
||||||
|
abstract val outlineLight: Color
|
||||||
|
abstract val outlineVariantLight: Color
|
||||||
|
abstract val scrimLight: Color
|
||||||
|
abstract val inverseSurfaceLight: Color
|
||||||
|
abstract val inverseOnSurfaceLight: Color
|
||||||
|
abstract val inversePrimaryLight: Color
|
||||||
|
abstract val surfaceDimLight: Color
|
||||||
|
abstract val surfaceBrightLight: Color
|
||||||
|
abstract val surfaceContainerLowestLight: Color
|
||||||
|
abstract val surfaceContainerLowLight: Color
|
||||||
|
abstract val surfaceContainerLight: Color
|
||||||
|
abstract val surfaceContainerHighLight: Color
|
||||||
|
abstract val surfaceContainerHighestLight: Color
|
||||||
|
// 深色
|
||||||
|
abstract val primaryDark: Color
|
||||||
|
abstract val onPrimaryDark: Color
|
||||||
|
abstract val primaryContainerDark: Color
|
||||||
|
abstract val onPrimaryContainerDark: Color
|
||||||
|
abstract val secondaryDark: Color
|
||||||
|
abstract val onSecondaryDark: Color
|
||||||
|
abstract val secondaryContainerDark: Color
|
||||||
|
abstract val onSecondaryContainerDark: Color
|
||||||
|
abstract val tertiaryDark: Color
|
||||||
|
abstract val onTertiaryDark: Color
|
||||||
|
abstract val tertiaryContainerDark: Color
|
||||||
|
abstract val onTertiaryContainerDark: Color
|
||||||
|
abstract val errorDark: Color
|
||||||
|
abstract val onErrorDark: Color
|
||||||
|
abstract val errorContainerDark: Color
|
||||||
|
abstract val onErrorContainerDark: Color
|
||||||
|
abstract val backgroundDark: Color
|
||||||
|
abstract val onBackgroundDark: Color
|
||||||
|
abstract val surfaceDark: Color
|
||||||
|
abstract val onSurfaceDark: Color
|
||||||
|
abstract val surfaceVariantDark: Color
|
||||||
|
abstract val onSurfaceVariantDark: Color
|
||||||
|
abstract val outlineDark: Color
|
||||||
|
abstract val outlineVariantDark: Color
|
||||||
|
abstract val scrimDark: Color
|
||||||
|
abstract val inverseSurfaceDark: Color
|
||||||
|
abstract val inverseOnSurfaceDark: Color
|
||||||
|
abstract val inversePrimaryDark: Color
|
||||||
|
abstract val surfaceDimDark: Color
|
||||||
|
abstract val surfaceBrightDark: Color
|
||||||
|
abstract val surfaceContainerLowestDark: Color
|
||||||
|
abstract val surfaceContainerLowDark: Color
|
||||||
|
abstract val surfaceContainerDark: Color
|
||||||
|
abstract val surfaceContainerHighDark: Color
|
||||||
|
abstract val surfaceContainerHighestDark: Color
|
||||||
|
|
||||||
open fun getCustomSliderActiveColor(): Color = Primary
|
// 默认主题 (蓝色)
|
||||||
open fun getCustomSliderInactiveColor(): Color = PrimaryContainer
|
|
||||||
|
|
||||||
// Default Theme (white)
|
|
||||||
object Default : ThemeColors() {
|
object Default : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFFFFFF)
|
override val primaryLight = Color(0xFF415F91)
|
||||||
override val Secondary = Color(0xFFF5F5F5)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFFE0E0E0)
|
override val primaryContainerLight = Color(0xFFD6E3FF)
|
||||||
override val OnPrimary = Color(0xFF616161)
|
override val onPrimaryContainerLight = Color(0xFF284777)
|
||||||
override val OnSecondary = Color(0xFF616161)
|
override val secondaryLight = Color(0xFF565F71)
|
||||||
override val OnTertiary = Color(0xFF616161)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFF5F5F5)
|
override val secondaryContainerLight = Color(0xFFDAE2F9)
|
||||||
override val SecondaryContainer = Color(0xFFEEEEEE)
|
override val onSecondaryContainerLight = Color(0xFF3E4759)
|
||||||
override val TertiaryContainer = Color(0xFFE0E0E0)
|
override val tertiaryLight = Color(0xFF705575)
|
||||||
override val OnPrimaryContainer = Color(0xFF000000)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF000000)
|
override val tertiaryContainerLight = Color(0xFFFAD8FD)
|
||||||
override val OnTertiaryContainer = Color(0xFF000000)
|
override val onTertiaryContainerLight = Color(0xFF573E5C)
|
||||||
override val ButtonContrast = Color(0xFF00BFFF)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFF9F9FF)
|
||||||
|
override val onBackgroundLight = Color(0xFF191C20)
|
||||||
|
override val surfaceLight = Color(0xFFF9F9FF)
|
||||||
|
override val onSurfaceLight = Color(0xFF191C20)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE0E2EC)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF44474E)
|
||||||
|
override val outlineLight = Color(0xFF74777F)
|
||||||
|
override val outlineVariantLight = Color(0xFFC4C6D0)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF2E3036)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF0F0F7)
|
||||||
|
override val inversePrimaryLight = Color(0xFFAAC7FF)
|
||||||
|
override val surfaceDimLight = Color(0xFFD9D9E0)
|
||||||
|
override val surfaceBrightLight = Color(0xFFF9F9FF)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFF3F3FA)
|
||||||
|
override val surfaceContainerLight = Color(0xFFEDEDF4)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFE7E8EE)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE2E2E9)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFAAC7FF)
|
||||||
|
override val onPrimaryDark = Color(0xFF0A305F)
|
||||||
|
override val primaryContainerDark = Color(0xFF284777)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFD6E3FF)
|
||||||
|
override val secondaryDark = Color(0xFFBEC6DC)
|
||||||
|
override val onSecondaryDark = Color(0xFF283141)
|
||||||
|
override val secondaryContainerDark = Color(0xFF3E4759)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFDAE2F9)
|
||||||
|
override val tertiaryDark = Color(0xFFDDBCE0)
|
||||||
|
override val onTertiaryDark = Color(0xFF3F2844)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF573E5C)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFAD8FD)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF111318)
|
||||||
|
override val onBackgroundDark = Color(0xFFE2E2E9)
|
||||||
|
override val surfaceDark = Color(0xFF111318)
|
||||||
|
override val onSurfaceDark = Color(0xFFE2E2E9)
|
||||||
|
override val surfaceVariantDark = Color(0xFF44474E)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC4C6D0)
|
||||||
|
override val outlineDark = Color(0xFF8E9099)
|
||||||
|
override val outlineVariantDark = Color(0xFF44474E)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE2E2E9)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF2E3036)
|
||||||
|
override val inversePrimaryDark = Color(0xFF415F91)
|
||||||
|
override val surfaceDimDark = Color(0xFF111318)
|
||||||
|
override val surfaceBrightDark = Color(0xFF37393E)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0C0E13)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF191C20)
|
||||||
|
override val surfaceContainerDark = Color(0xFF1D2024)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF282A2F)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF33353A)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blue Theme
|
// 绿色主题
|
||||||
object Blue : ThemeColors() {
|
|
||||||
override val Primary = Color(0xFF2196F3)
|
|
||||||
override val Secondary = Color(0xFF1E88E5)
|
|
||||||
override val Tertiary = Color(0xFF0D47A1)
|
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
|
||||||
override val PrimaryContainer = Color(0xFFCBE6FC)
|
|
||||||
override val SecondaryContainer = Color(0xFFBBDEFB)
|
|
||||||
override val TertiaryContainer = Color(0xFF90CAF9)
|
|
||||||
override val OnPrimaryContainer = Color(0xFF0A1A2E)
|
|
||||||
override val OnSecondaryContainer = Color(0xFF0A192D)
|
|
||||||
override val OnTertiaryContainer = Color(0xFF071B3D)
|
|
||||||
override val ButtonContrast = Color(0xFF00BFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Green Theme
|
|
||||||
object Green : ThemeColors() {
|
object Green : ThemeColors() {
|
||||||
override val Primary = Color(0xFF4CAF50)
|
override val primaryLight = Color(0xFF4C662B)
|
||||||
override val Secondary = Color(0xFF43A047)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF1B5E20)
|
override val primaryContainerLight = Color(0xFFCDEDA3)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF354E16)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF586249)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFC8E6C9)
|
override val secondaryContainerLight = Color(0xFFDCE7C8)
|
||||||
override val SecondaryContainer = Color(0xFFA5D6A7)
|
override val onSecondaryContainerLight = Color(0xFF404A33)
|
||||||
override val TertiaryContainer = Color(0xFF81C784)
|
override val tertiaryLight = Color(0xFF386663)
|
||||||
override val OnPrimaryContainer = Color(0xFF0A1F0B)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF0A1D0B)
|
override val tertiaryContainerLight = Color(0xFFBCECE7)
|
||||||
override val OnTertiaryContainer = Color(0xFF071F09)
|
override val onTertiaryContainerLight = Color(0xFF1F4E4B)
|
||||||
override val ButtonContrast = Color(0xFF32CD32)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFF9FAEF)
|
||||||
|
override val onBackgroundLight = Color(0xFF1A1C16)
|
||||||
|
override val surfaceLight = Color(0xFFF9FAEF)
|
||||||
|
override val onSurfaceLight = Color(0xFF1A1C16)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE1E4D5)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF44483D)
|
||||||
|
override val outlineLight = Color(0xFF75796C)
|
||||||
|
override val outlineVariantLight = Color(0xFFC5C8BA)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF2F312A)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF1F2E6)
|
||||||
|
override val inversePrimaryLight = Color(0xFFB1D18A)
|
||||||
|
override val surfaceDimLight = Color(0xFFDADBD0)
|
||||||
|
override val surfaceBrightLight = Color(0xFFF9FAEF)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFF3F4E9)
|
||||||
|
override val surfaceContainerLight = Color(0xFFEEEFE3)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFE8E9DE)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE2E3D8)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFB1D18A)
|
||||||
|
override val onPrimaryDark = Color(0xFF1F3701)
|
||||||
|
override val primaryContainerDark = Color(0xFF354E16)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFCDEDA3)
|
||||||
|
override val secondaryDark = Color(0xFFBFCBAD)
|
||||||
|
override val onSecondaryDark = Color(0xFF2A331E)
|
||||||
|
override val secondaryContainerDark = Color(0xFF404A33)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFDCE7C8)
|
||||||
|
override val tertiaryDark = Color(0xFFA0D0CB)
|
||||||
|
override val onTertiaryDark = Color(0xFF003735)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF1F4E4B)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFBCECE7)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF12140E)
|
||||||
|
override val onBackgroundDark = Color(0xFFE2E3D8)
|
||||||
|
override val surfaceDark = Color(0xFF12140E)
|
||||||
|
override val onSurfaceDark = Color(0xFFE2E3D8)
|
||||||
|
override val surfaceVariantDark = Color(0xFF44483D)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC5C8BA)
|
||||||
|
override val outlineDark = Color(0xFF8F9285)
|
||||||
|
override val outlineVariantDark = Color(0xFF44483D)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE2E3D8)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF2F312A)
|
||||||
|
override val inversePrimaryDark = Color(0xFF4C662B)
|
||||||
|
override val surfaceDimDark = Color(0xFF12140E)
|
||||||
|
override val surfaceBrightDark = Color(0xFF383A32)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0C0F09)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1A1C16)
|
||||||
|
override val surfaceContainerDark = Color(0xFF1E201A)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF282B24)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF33362E)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Purple Theme
|
// 紫色主题
|
||||||
object Purple : ThemeColors() {
|
object Purple : ThemeColors() {
|
||||||
override val Primary = Color(0xFF9C27B0)
|
override val primaryLight = Color(0xFF7C4E7E)
|
||||||
override val Secondary = Color(0xFF8E24AA)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF4A148C)
|
override val primaryContainerLight = Color(0xFFFFD6FC)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF623765)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF6C586B)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFE1BEE7)
|
override val secondaryContainerLight = Color(0xFFF5DBF1)
|
||||||
override val SecondaryContainer = Color(0xFFCE93D8)
|
override val onSecondaryContainerLight = Color(0xFF534152)
|
||||||
override val TertiaryContainer = Color(0xFFB39DDB)
|
override val tertiaryLight = Color(0xFF825249)
|
||||||
override val OnPrimaryContainer = Color(0xFF1F0A23)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1C0A21)
|
override val tertiaryContainerLight = Color(0xFFFFDAD4)
|
||||||
override val OnTertiaryContainer = Color(0xFF12071C)
|
override val onTertiaryContainerLight = Color(0xFF673B33)
|
||||||
override val ButtonContrast = Color(0xFFDA70D6)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF7FA)
|
||||||
|
override val onBackgroundLight = Color(0xFF1F1A1F)
|
||||||
|
override val surfaceLight = Color(0xFFFFF7FA)
|
||||||
|
override val onSurfaceLight = Color(0xFF1F1A1F)
|
||||||
|
override val surfaceVariantLight = Color(0xFFEDDFE8)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF4D444C)
|
||||||
|
override val outlineLight = Color(0xFF7F747C)
|
||||||
|
override val outlineVariantLight = Color(0xFFD0C3CC)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF352F34)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF9EEF4)
|
||||||
|
override val inversePrimaryLight = Color(0xFFECB4EC)
|
||||||
|
override val surfaceDimLight = Color(0xFFE2D7DE)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF7FA)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFCF0F7)
|
||||||
|
override val surfaceContainerLight = Color(0xFFF6EBF2)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFF0E5EC)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFEBDFE6)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFECB4EC)
|
||||||
|
override val onPrimaryDark = Color(0xFF49204D)
|
||||||
|
override val primaryContainerDark = Color(0xFF623765)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFFFD6FC)
|
||||||
|
override val secondaryDark = Color(0xFFD8BFD5)
|
||||||
|
override val onSecondaryDark = Color(0xFF3B2B3B)
|
||||||
|
override val secondaryContainerDark = Color(0xFF534152)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFF5DBF1)
|
||||||
|
override val tertiaryDark = Color(0xFFF6B8AD)
|
||||||
|
override val onTertiaryDark = Color(0xFF4C251F)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF673B33)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFFDAD4)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF171216)
|
||||||
|
override val onBackgroundDark = Color(0xFFEBDFE6)
|
||||||
|
override val surfaceDark = Color(0xFF171216)
|
||||||
|
override val onSurfaceDark = Color(0xFFEBDFE6)
|
||||||
|
override val surfaceVariantDark = Color(0xFF4D444C)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFD0C3CC)
|
||||||
|
override val outlineDark = Color(0xFF998D96)
|
||||||
|
override val outlineVariantDark = Color(0xFF4D444C)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFEBDFE6)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF352F34)
|
||||||
|
override val inversePrimaryDark = Color(0xFF7C4E7E)
|
||||||
|
override val surfaceDimDark = Color(0xFF171216)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3E373D)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF110D11)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1F1A1F)
|
||||||
|
override val surfaceContainerDark = Color(0xFF231E23)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2E282D)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF393338)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Orange Theme
|
// 橙色主题
|
||||||
object Orange : ThemeColors() {
|
object Orange : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFF9800)
|
override val primaryLight = Color(0xFF8B4F24)
|
||||||
override val Secondary = Color(0xFFFB8C00)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFFE65100)
|
override val primaryContainerLight = Color(0xFFFFDCC7)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF6E390E)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF755846)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFFE0B2)
|
override val secondaryContainerLight = Color(0xFFFFDCC7)
|
||||||
override val SecondaryContainer = Color(0xFFFFCC80)
|
override val onSecondaryContainerLight = Color(0xFF5B4130)
|
||||||
override val TertiaryContainer = Color(0xFFFFB74D)
|
override val tertiaryLight = Color(0xFF865219)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1100)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1A1000)
|
override val tertiaryContainerLight = Color(0xFFFFDCBF)
|
||||||
override val OnTertiaryContainer = Color(0xFF1A0B00)
|
override val onTertiaryContainerLight = Color(0xFF6A3B01)
|
||||||
override val ButtonContrast = Color(0xFFFF6347)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF8F5)
|
||||||
|
override val onBackgroundLight = Color(0xFF221A15)
|
||||||
|
override val surfaceLight = Color(0xFFFFF8F5)
|
||||||
|
override val onSurfaceLight = Color(0xFF221A15)
|
||||||
|
override val surfaceVariantLight = Color(0xFFF4DED3)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF52443C)
|
||||||
|
override val outlineLight = Color(0xFF84746A)
|
||||||
|
override val outlineVariantLight = Color(0xFFD7C3B8)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF382E29)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFFFEDE5)
|
||||||
|
override val inversePrimaryLight = Color(0xFFFFB787)
|
||||||
|
override val surfaceDimLight = Color(0xFFE7D7CE)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF8F5)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFFF1EA)
|
||||||
|
override val surfaceContainerLight = Color(0xFFFCEBE2)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFF6E5DC)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFF0DFD7)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFFFB787)
|
||||||
|
override val onPrimaryDark = Color(0xFF502400)
|
||||||
|
override val primaryContainerDark = Color(0xFF6E390E)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFFFDCC7)
|
||||||
|
override val secondaryDark = Color(0xFFE5BFA8)
|
||||||
|
override val onSecondaryDark = Color(0xFF422B1B)
|
||||||
|
override val secondaryContainerDark = Color(0xFF5B4130)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFFFDCC7)
|
||||||
|
override val tertiaryDark = Color(0xFFFDB876)
|
||||||
|
override val onTertiaryDark = Color(0xFF4B2800)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF6A3B01)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFFDCBF)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF19120D)
|
||||||
|
override val onBackgroundDark = Color(0xFFF0DFD7)
|
||||||
|
override val surfaceDark = Color(0xFF19120D)
|
||||||
|
override val onSurfaceDark = Color(0xFFF0DFD7)
|
||||||
|
override val surfaceVariantDark = Color(0xFF52443C)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFD7C3B8)
|
||||||
|
override val outlineDark = Color(0xFF9F8D83)
|
||||||
|
override val outlineVariantDark = Color(0xFF52443C)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFF0DFD7)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF382E29)
|
||||||
|
override val inversePrimaryDark = Color(0xFF8B4F24)
|
||||||
|
override val surfaceDimDark = Color(0xFF19120D)
|
||||||
|
override val surfaceBrightDark = Color(0xFF413731)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF140D08)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF221A15)
|
||||||
|
override val surfaceContainerDark = Color(0xFF261E19)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF312823)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF3D332D)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pink Theme
|
// 粉色主题
|
||||||
object Pink : ThemeColors() {
|
object Pink : ThemeColors() {
|
||||||
override val Primary = Color(0xFFE91E63)
|
override val primaryLight = Color(0xFF8C4A60)
|
||||||
override val Secondary = Color(0xFFD81B60)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF880E4F)
|
override val primaryContainerLight = Color(0xFFFFD9E2)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF703348)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF8B4A62)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFF8BBD0)
|
override val secondaryContainerLight = Color(0xFFFFD9E3)
|
||||||
override val SecondaryContainer = Color(0xFFF48FB1)
|
override val onSecondaryContainerLight = Color(0xFF6F334B)
|
||||||
override val TertiaryContainer = Color(0xFFE91E63)
|
override val tertiaryLight = Color(0xFF8B4A62)
|
||||||
override val OnPrimaryContainer = Color(0xFF2E0A14)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF2B0A13)
|
override val tertiaryContainerLight = Color(0xFFFFD9E3)
|
||||||
override val OnTertiaryContainer = Color(0xFF1C0311)
|
override val onTertiaryContainerLight = Color(0xFF6F334B)
|
||||||
override val ButtonContrast = Color(0xFFFF1493)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF8F8)
|
||||||
|
override val onBackgroundLight = Color(0xFF22191B)
|
||||||
|
override val surfaceLight = Color(0xFFFFF8F8)
|
||||||
|
override val onSurfaceLight = Color(0xFF22191B)
|
||||||
|
override val surfaceVariantLight = Color(0xFFF2DDE1)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF514346)
|
||||||
|
override val outlineLight = Color(0xFF837377)
|
||||||
|
override val outlineVariantLight = Color(0xFFD5C2C5)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF372E30)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFFDEDEF)
|
||||||
|
override val inversePrimaryLight = Color(0xFFFFB1C7)
|
||||||
|
override val surfaceDimLight = Color(0xFFE6D6D9)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF8F8)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFFF0F2)
|
||||||
|
override val surfaceContainerLight = Color(0xFFFBEAED)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFF5E4E7)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFEFDFE1)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFFFB1C7)
|
||||||
|
override val onPrimaryDark = Color(0xFF541D32)
|
||||||
|
override val primaryContainerDark = Color(0xFF703348)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFFFD9E2)
|
||||||
|
override val secondaryDark = Color(0xFFFFB0CB)
|
||||||
|
override val onSecondaryDark = Color(0xFF541D34)
|
||||||
|
override val secondaryContainerDark = Color(0xFF6F334B)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFFFD9E3)
|
||||||
|
override val tertiaryDark = Color(0xFFFFB0CB)
|
||||||
|
override val onTertiaryDark = Color(0xFF541D34)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF6F334B)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFFFD9E3)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF191113)
|
||||||
|
override val onBackgroundDark = Color(0xFFEFDFE1)
|
||||||
|
override val surfaceDark = Color(0xFF191113)
|
||||||
|
override val onSurfaceDark = Color(0xFFEFDFE1)
|
||||||
|
override val surfaceVariantDark = Color(0xFF514346)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFD5C2C5)
|
||||||
|
override val outlineDark = Color(0xFF9E8C90)
|
||||||
|
override val outlineVariantDark = Color(0xFF514346)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFEFDFE1)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF372E30)
|
||||||
|
override val inversePrimaryDark = Color(0xFF8C4A60)
|
||||||
|
override val surfaceDimDark = Color(0xFF191113)
|
||||||
|
override val surfaceBrightDark = Color(0xFF413739)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF140C0E)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF22191B)
|
||||||
|
override val surfaceContainerDark = Color(0xFF261D1F)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF31282A)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF3C3234)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gray Theme
|
// 灰色主题
|
||||||
object Gray : ThemeColors() {
|
object Gray : ThemeColors() {
|
||||||
override val Primary = Color(0xFF9E9E9E)
|
override val primaryLight = Color(0xFF5B5C5C)
|
||||||
override val Secondary = Color(0xFF757575)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF616161)
|
override val primaryContainerLight = Color(0xFF747474)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFFFEFCFC)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF5F5E5E)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFEEEEEE)
|
override val secondaryContainerLight = Color(0xFFE4E2E1)
|
||||||
override val SecondaryContainer = Color(0xFFE0E0E0)
|
override val onSecondaryContainerLight = Color(0xFF656464)
|
||||||
override val TertiaryContainer = Color(0xFFBDBDBD)
|
override val tertiaryLight = Color(0xFF5E5B5D)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1A1A)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF171717)
|
override val tertiaryContainerLight = Color(0xFF777375)
|
||||||
override val OnTertiaryContainer = Color(0xFF141414)
|
override val onTertiaryContainerLight = Color(0xFFFFFBFF)
|
||||||
override val ButtonContrast = Color(0xFF696969)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFCF8F8)
|
||||||
|
override val onBackgroundLight = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceLight = Color(0xFFFCF8F8)
|
||||||
|
override val onSurfaceLight = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE0E3E3)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF444748)
|
||||||
|
override val outlineLight = Color(0xFF747878)
|
||||||
|
override val outlineVariantLight = Color(0xFFC4C7C7)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF313030)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF4F0EF)
|
||||||
|
override val inversePrimaryLight = Color(0xFFC7C6C6)
|
||||||
|
override val surfaceDimLight = Color(0xFFDDD9D8)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFCF8F8)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFF7F3F2)
|
||||||
|
override val surfaceContainerLight = Color(0xFFF1EDEC)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFEBE7E7)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE5E2E1)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFC7C6C6)
|
||||||
|
override val onPrimaryDark = Color(0xFF303031)
|
||||||
|
override val primaryContainerDark = Color(0xFF919190)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFF161718)
|
||||||
|
override val secondaryDark = Color(0xFFC8C6C5)
|
||||||
|
override val onSecondaryDark = Color(0xFF303030)
|
||||||
|
override val secondaryContainerDark = Color(0xFF474746)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFB7B5B4)
|
||||||
|
override val tertiaryDark = Color(0xFFCAC5C7)
|
||||||
|
override val onTertiaryDark = Color(0xFF323031)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF948F91)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFF181718)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF141313)
|
||||||
|
override val onBackgroundDark = Color(0xFFE5E2E1)
|
||||||
|
override val surfaceDark = Color(0xFF141313)
|
||||||
|
override val onSurfaceDark = Color(0xFFE5E2E1)
|
||||||
|
override val surfaceVariantDark = Color(0xFF444748)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFC4C7C7)
|
||||||
|
override val outlineDark = Color(0xFF8E9192)
|
||||||
|
override val outlineVariantDark = Color(0xFF444748)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE5E2E1)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF313030)
|
||||||
|
override val inversePrimaryDark = Color(0xFF5E5E5E)
|
||||||
|
override val surfaceDimDark = Color(0xFF141313)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3A3939)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF0E0E0E)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1C1B1B)
|
||||||
|
override val surfaceContainerDark = Color(0xFF201F1F)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2A2A2A)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF353434)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 黄色主题
|
||||||
object Yellow : ThemeColors() {
|
object Yellow : ThemeColors() {
|
||||||
override val Primary = Color(0xFFFFD700)
|
override val primaryLight = Color(0xFF6D5E0F)
|
||||||
override val Secondary = Color(0xFFFFBC52)
|
override val onPrimaryLight = Color(0xFFFFFFFF)
|
||||||
override val Tertiary = Color(0xFF795548)
|
override val primaryContainerLight = Color(0xFFF8E288)
|
||||||
override val OnPrimary = Color(0xFFFFFFFF)
|
override val onPrimaryContainerLight = Color(0xFF534600)
|
||||||
override val OnSecondary = Color(0xFFFFFFFF)
|
override val secondaryLight = Color(0xFF6D5E0F)
|
||||||
override val OnTertiary = Color(0xFFFFFFFF)
|
override val onSecondaryLight = Color(0xFFFFFFFF)
|
||||||
override val PrimaryContainer = Color(0xFFFFF7D6)
|
override val secondaryContainerLight = Color(0xFFF7E388)
|
||||||
override val SecondaryContainer = Color(0xFFFFE6B3)
|
override val onSecondaryContainerLight = Color(0xFF534600)
|
||||||
override val TertiaryContainer = Color(0xFFD7CCC8)
|
override val tertiaryLight = Color(0xFF685F13)
|
||||||
override val OnPrimaryContainer = Color(0xFF1A1600)
|
override val onTertiaryLight = Color(0xFFFFFFFF)
|
||||||
override val OnSecondaryContainer = Color(0xFF1A1100)
|
override val tertiaryContainerLight = Color(0xFFF1E58A)
|
||||||
override val OnTertiaryContainer = Color(0xFF1A1717)
|
override val onTertiaryContainerLight = Color(0xFF4F4800)
|
||||||
override val ButtonContrast = Color(0xFFFFD700)
|
override val errorLight = Color(0xFFBA1A1A)
|
||||||
|
override val onErrorLight = Color(0xFFFFFFFF)
|
||||||
|
override val errorContainerLight = Color(0xFFFFDAD6)
|
||||||
|
override val onErrorContainerLight = Color(0xFF93000A)
|
||||||
|
override val backgroundLight = Color(0xFFFFF9ED)
|
||||||
|
override val onBackgroundLight = Color(0xFF1E1C13)
|
||||||
|
override val surfaceLight = Color(0xFFFFF9ED)
|
||||||
|
override val onSurfaceLight = Color(0xFF1E1C13)
|
||||||
|
override val surfaceVariantLight = Color(0xFFE9E2D0)
|
||||||
|
override val onSurfaceVariantLight = Color(0xFF4B4739)
|
||||||
|
override val outlineLight = Color(0xFF7C7768)
|
||||||
|
override val outlineVariantLight = Color(0xFFCDC6B4)
|
||||||
|
override val scrimLight = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceLight = Color(0xFF333027)
|
||||||
|
override val inverseOnSurfaceLight = Color(0xFFF7F0E2)
|
||||||
|
override val inversePrimaryLight = Color(0xFFDAC66F)
|
||||||
|
override val surfaceDimLight = Color(0xFFE0D9CC)
|
||||||
|
override val surfaceBrightLight = Color(0xFFFFF9ED)
|
||||||
|
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||||
|
override val surfaceContainerLowLight = Color(0xFFFAF3E5)
|
||||||
|
override val surfaceContainerLight = Color(0xFFF4EDDF)
|
||||||
|
override val surfaceContainerHighLight = Color(0xFFEEE8DA)
|
||||||
|
override val surfaceContainerHighestLight = Color(0xFFE8E2D4)
|
||||||
|
|
||||||
|
override val primaryDark = Color(0xFFDAC66F)
|
||||||
|
override val onPrimaryDark = Color(0xFF393000)
|
||||||
|
override val primaryContainerDark = Color(0xFF534600)
|
||||||
|
override val onPrimaryContainerDark = Color(0xFFF8E288)
|
||||||
|
override val secondaryDark = Color(0xFFDAC76F)
|
||||||
|
override val onSecondaryDark = Color(0xFF393000)
|
||||||
|
override val secondaryContainerDark = Color(0xFF534600)
|
||||||
|
override val onSecondaryContainerDark = Color(0xFFF7E388)
|
||||||
|
override val tertiaryDark = Color(0xFFD4C871)
|
||||||
|
override val onTertiaryDark = Color(0xFF363100)
|
||||||
|
override val tertiaryContainerDark = Color(0xFF4F4800)
|
||||||
|
override val onTertiaryContainerDark = Color(0xFFF1E58A)
|
||||||
|
override val errorDark = Color(0xFFFFB4AB)
|
||||||
|
override val onErrorDark = Color(0xFF690005)
|
||||||
|
override val errorContainerDark = Color(0xFF93000A)
|
||||||
|
override val onErrorContainerDark = Color(0xFFFFDAD6)
|
||||||
|
override val backgroundDark = Color(0xFF15130B)
|
||||||
|
override val onBackgroundDark = Color(0xFFE8E2D4)
|
||||||
|
override val surfaceDark = Color(0xFF15130B)
|
||||||
|
override val onSurfaceDark = Color(0xFFE8E2D4)
|
||||||
|
override val surfaceVariantDark = Color(0xFF4B4739)
|
||||||
|
override val onSurfaceVariantDark = Color(0xFFCDC6B4)
|
||||||
|
override val outlineDark = Color(0xFF969080)
|
||||||
|
override val outlineVariantDark = Color(0xFF4B4739)
|
||||||
|
override val scrimDark = Color(0xFF000000)
|
||||||
|
override val inverseSurfaceDark = Color(0xFFE8E2D4)
|
||||||
|
override val inverseOnSurfaceDark = Color(0xFF333027)
|
||||||
|
override val inversePrimaryDark = Color(0xFF6D5E0F)
|
||||||
|
override val surfaceDimDark = Color(0xFF15130B)
|
||||||
|
override val surfaceBrightDark = Color(0xFF3C3930)
|
||||||
|
override val surfaceContainerLowestDark = Color(0xFF100E07)
|
||||||
|
override val surfaceContainerLowDark = Color(0xFF1E1C13)
|
||||||
|
override val surfaceContainerDark = Color(0xFF222017)
|
||||||
|
override val surfaceContainerHighDark = Color(0xFF2C2A21)
|
||||||
|
override val surfaceContainerHighestDark = Color(0xFF37352B)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
fun fromName(name: String): ThemeColors = when (name.lowercase()) {
|
||||||
"blue" -> Blue
|
|
||||||
"green" -> Green
|
"green" -> Green
|
||||||
"purple" -> Purple
|
"purple" -> Purple
|
||||||
"orange" -> Orange
|
"orange" -> Orange
|
||||||
"pink" -> Pink
|
"pink" -> Pink
|
||||||
"gray" -> Gray
|
"gray" -> Gray
|
||||||
"white" -> Yellow
|
"yellow" -> Yellow
|
||||||
else -> Default
|
else -> Default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user