253 Commits

Author SHA1 Message Date
ShirkNeko
b8b75349e0 kernel: Bump KSU_VERSION_API to 3.1.9 2025-08-24 11:45:08 +08:00
dabao1955
3f3e520641 kernel: Remove unnecessary strip in CONFIG_KSU_TRACEPOINT_HOOK check (#353)
The 'strip' function is redundant when checking Kconfig variables, as
values from CONFIG options (like CONFIG_KSU_TRACEPOINT_HOOK) are already
trimmed and do not contain leading/trailing whitespace.

Simplify the condition for better readability and maintainability:
  - Remove unnecessary $(strip ...)
  - Add consistent spacing around the comma

This change aligns with kernel Makefile conventions and improves code clarity
without altering behavior.

Signed-off-by: dabao1955 <dabao1955@163.com>
2025-08-24 11:41:18 +08:00
OukaroMF
21aa58f1ee docs: EN: add simple translation for tracepoint-hook docs (#352)
* add EN translation for tracepoint-hook

* add EN translation for tracepoint-hook
2025-08-24 11:38:04 +08:00
ShirkNeko
da164ebeed kernel: Modified dynamic signature All files have been renamed to the correct names: Dynamic Manager 2025-08-24 11:00:17 +08:00
YC酱luyancib
35ae324df5 docs: zh: Introduce tracepoint hook documents (#350)
* docs: Introduce tracepoint hook documents

Still working in progress...
Only zh-cn.

* fix broken link

* format

* 更新 tracepoint-hook.md

* format

* 更新 tracepoint-hook.md

* docs: zh: fmt

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

---------

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
Co-authored-by: WenHao2130 <wenhao2130@outlook.com>
2025-08-24 00:06:13 +08:00
rsuntk
fdda1f4ec0 ksud: strip resetprop
* Upstream resetprop is intentionally not stripped.

Signed-off-by: rsuntk <rsuntk@yukiprjkt.my.id>
2025-08-23 17:03:31 +08:00
愛莉希雅
770ed1fdf2 manager: Updated Chinese Traditional 2025-08-20 20:55:35 +08:00
ShirkNeko
106c10d6f8 Kernel: Improved permission tracking logic when dynamic signature manager existence checks are enabled
- Fixed a panic issue caused by repeated scans in certain cases where dynamic signatures were not enabled

Revert: setup.sh: cleanup symlink & config logic (#338)
2025-08-19 18:16:09 +08:00
Juno Bủh
f10de68deb Fix typo strings (#339)
It's annoying that some parts have dot at the end of sentences and some don't. So I think it's better to remove all dot at the end of sentences to make it more consistent and easier to read
2025-08-18 17:06:15 +08:00
noctyra
44db32e8de setup.sh: cleanup symlink & config logic (#338)
- Avoid creating nested symlink (KernelSU/kernel/kernel) when running setup multiple times
- Cleanup in Kconfig only removes the correct line
- Makefile/Kconfig updates only applied once with clearer logs
2025-08-18 17:05:51 +08:00
ShirkNeko
3ced30b427 kernel: refactor APK signature verification functions for clarity and efficiency 2025-08-17 23:10:51 +08:00
ShirkNeko
624a8d9f86 kernel: selinux: dontaudit untrusted_app su dir { getattr }
* Following the advice that was given by member in rksu group, by replacing ALL to untrusted_app.

$ /system/bin/stat /proc/1
Result:
08-15 14:57:54.370 20062 20062 W stat    : type=1400 audit(0.0:9564): avc:  denied  { getattr } for  path="/proc/1" dev="proc" ino=12308 scontext=u:r:untrusted_app_27:s0:c27,c258,c512,c768 tcontext=u:r:init:s0 tclass=dir permissive=0 app=com.termux
(issue
438bd5f#commitcomment-163785768)

Test: Checker pass.

* Any issue? Let me know.

Tested-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
2025-08-16 12:42:19 +08:00
NkBe
16007f5892 [skip ci]manger: Fix translate module signature verification string (#332)
* manger: Fix translate module signature verification string

Revised the 'module_signature_verification_summary' string in multiple languages to clarify that forced signature verification applies to all ARM architectures, not just arm64-v8a. This improves accuracy and consistency across translations.

* manger: Update Traditional Chinese (Taiwan)

* manger: Update Traditional Chinese (Cantonese)
2025-08-16 12:34:50 +08:00
ShirkNeko
6bb83fdb07 kernel: Add KSU_MANUAL_HOOK configuration for manual KernelSU syscall hooking 2025-08-15 23:00:11 +08:00
ShirkNeko
ec6991f98b kernel: refactor apply_kernelsu_rules to use mutex for synchronization 2025-08-15 11:00:34 +08:00
Tashfin Shakeer Rhythm
f6337e2d52 kernel: selinux: rules: Micro-optimize get_policydb() and fix illegal RCU lock usage in handle_sepolicy() (#2695)
Signed-off-by: Tashfin Shakeer Rhythm <tashfinshakeerrhythm@gmail.com>
2025-08-15 10:33:05 +08:00
ShirkNeko
6b2bf23946 [skip ci]kernel: Add support for ksu_devpts_hook handling in ksu tracepoints 2025-08-15 10:32:23 +08:00
ShirkNeko
dbc662486b Fix correct hook type acquisition 2025-08-14 23:18:21 +08:00
ShirkNeko
e96194c7ff manager: Add hook type info 2025-08-14 22:59:37 +08:00
ShirkNeko
49b01aad74 kernel: Introducing Tracepoint Hook Type Support
Tracepoint is a predefined hook point in the kernel, compared to Kprobe,
it is more stable and has lower performance overhead, although compatibility
is relatively poor, it is still worth trying

By the way, we have also included the config definitions related to hook types
in Kconfig, to enhance cleanliness

Improve and merge types that do not require hooks

Introducing the hook type prctl

These patches is based on https://github.com/backslashxx/KernelSU/issues/5

Co-authored-by: Cloud_Yun <1770669041@qq.com>
Co-authored-by: Prslc <prslc113@gmail.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-08-14 22:17:02 +08:00
ShirkNeko
656a23a250 New Crowdin updates (#329)
* New translations strings.xml (Japanese)

* New translations strings.xml (Russian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-08-14 17:26:03 +08:00
ShirkNeko
11a628f536 manager: Update zakosign binary with support for arm32 architecture 2025-08-14 13:07:58 +08:00
ShirkNeko
57fcf86579 Updating zakosign binary script builds 2025-08-12 19:37:27 +08:00
ShirkNeko
ecb2dae743 manager: Fix anomaly in displaying system app functions due to status refresh issue. 2025-08-12 13:45:27 +08:00
ShirkNeko
75e0cd05a9 manager: Add recognition for app paths add uid hide 2025-08-12 13:25:36 +08:00
WenHao2130
1eb0f19ca6 manager: strings: fmt
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-08-12 00:16:52 +08:00
NkBe
a7ee0423a3 manager: Updated Chinese Traditional (#325)
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-08-12 00:13:21 +08:00
rsuntk
355e1c648a kernel: selinux: dontaudit * su dir getattr
* Likely a detection point for newer android.

* I am not sure about this, but a module try to address this: https://github.com/aviraxp/ZN-AuditPatch

* Need more testing.

Suggested-by: fatalcoder524 <11532648+fatalcoder524@users.noreply.github.com>
Tested-by: rsuntk <rsuntk@yukiprjkt.my.id>
Signed-off-by: rsuntk <rsuntk@yukiprjkt.my.id>
2025-08-11 17:18:01 +08:00
ShirkNeko
3cde3e1659 manger: simplify and Using array splitting to compare full version numbers 2025-08-10 19:19:40 +08:00
rsuntk
8dcc3f7c46 ksud: bump resetprop from Magisk v30.2
* Related pr:
https://github.com/tiann/KernelSU/pull/2700

rsuntk: Drop x86_64 bin, build our own arm bin.

Signed-off-by: rsuntk <rsuntk@yukiprjkt.my.id>
2025-08-10 18:32:39 +08:00
5ec1cff
a041b90891 ksud: make clippy happy (#2683)
e1be06240d/clippy_lints/src/format_args.rs (L168)
2025-08-10 17:59:12 +08:00
ShirkNeko
c1c648e34d [skip ci]ksud: support vendor_boot patching for some odd devices (#2650)
This will add support to patch vendor_boot with LKM for devices which
have their init ramdisk inside of vendor_boot and not boot/init_boot.

---------

Co-authored-by: Rifat Azad <rifat.44.azad.rifs@gmail.com>
Co-authored-by: 5ec1cff <ewtqyqyewtqyqy@gmail.com>
Co-authored-by: 5ec1cff <56485584+5ec1cff@users.noreply.github.com>
2025-08-10 17:31:43 +08:00
ShirkNeko
0754fc8920 manager: Update susfs binary version
- Add switches and descriptions for new AVC log spoofing in basic settings
2025-08-10 17:17:28 +08:00
NkBe
4d3bae1113 [skip ci]manger: Update Traditional Chinese (Taiwan) (#314) 2025-08-10 15:55:43 +08:00
Prslc
0fdd7d437f kernel(kpm): Fix typo in printk logs (#311)
Signed-off-by: Prslc <prslc113@gmail.com>
2025-08-06 17:02:38 +08:00
ShirkNeko
6e89c81407 manager: Cleaning up the code 2025-08-03 20:05:19 +08:00
ShirkNeko
f8b8c7f671 manager: Update agp and dependency version, improve module signature error return 2025-08-03 19:08:57 +08:00
ShirkNeko
cd4edf97bd manager: Refinement of module signatures again 2025-08-03 18:50:20 +08:00
Juno Bủh
e3f1e49fe1 Update Vietnamese Translation 2025-08-03 12:31:00 +05:30
MiRinChan
abe0dee4da string: Change the description of Signature Verification 2025-08-03 12:30:32 +05:30
MiRinChan
bc3fcec514 strings: Modified the description of resetting the Bootloader lock 2025-08-03 12:30:32 +05:30
ShirkNeko
d225f0bae9 manager: Continue to improve module signatures 2025-08-03 05:39:35 +08:00
ShirkNeko
48d7a13028 manager: add module signature verification
fix site base
2025-08-03 04:34:04 +08:00
ShirkNeko
7e7d2a28af kernel: Clean up the Dynamic Signature Manager related functionality, removing unnecessary rescanning logic 2025-08-02 16:28:52 +08:00
ShirkNeko
acfba3d0f8 kernel: refactoring uses flag bits for exiting instead of cleaning up the workflow 2025-08-02 01:27:02 +08:00
ShirkNeko
bf5a8a8909 kernel: optimize dynamic signature operation, enhance error recovery capability
- Add timeout protection for multi-manager APK verification.
2025-08-01 16:55:13 +08:00
ShirkNeko
7c4d8da7d1 New Crowdin updates (#300)
* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)
2025-07-31 01:07:09 +08:00
Juno Bủh
c656d87e42 [skip ci]Update Vietnamese Translation (#302)
* Update Vietnamese Translation

* Update Vietnamese Translation
2025-07-31 01:05:39 +08:00
ShirkNeko
1b7c7fd726 manager: Optimize SuSFS path editing functionality
- Resize thread pool to improve performance
2025-07-31 01:00:49 +08:00
Seyud
ea68183f80 revert: fix: Remove the incorrect display of the KPM version and simplify the checking logic (#299) 2025-07-29 20:26:19 +08:00
Seyud
b1ee07fee1 Improve file existence check and copying methods, optimize command execution logic, and fix KPM version display issue (#298)
* fix: Remove the incorrect display of the KPM version and simplify the checking logic

* refactor: Optimize the command execution logic and simplify the code structure.

* fix: Improve the file existence check and file copying methods.
2025-07-29 15:16:34 +08:00
ShirkNeko
c42b4ffe4b kernel: fix dynamic signature rescan functionality and update related handlers 2025-07-29 11:49:36 +08:00
ShirkNeko
cfd070f33c kernel: Add Hot Update Rescan Manager feature for dynamic signatures 2025-07-29 10:32:15 +08:00
ShirkNeko
3ae1a3b10a kermel: Refactoring and separating dynamic signature-related structures and functions 2025-07-28 22:58:48 +08:00
ShirkNeko
d21f92d817 ksu_susfs: Reduced the binary size by compiling with dynamically linked, and -O3 for speed 2025-07-28 18:07:05 +08:00
ShirkNeko
6bb66e2819 kernel: Updated APK signature support, removed redundant definitions and adjusted indexing
Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-07-28 17:12:17 +08:00
WenHao2130
115206bcc6 manager: Remove Loading Dialog when enable/disable the module (#291)
Magic Mount is fast enough that there is almost no
need to display a loading animation while waiting
for the backend to finish processing.

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-07-24 19:43:56 +08:00
ShirkNeko
5ba4f73eeb manager: Update to version 3.1.8 and rename the relevant version checking functions 2025-07-21 21:22:07 +08:00
ShirkNeko
139899d05d [skip ci]manager: Add SUS loop path function 2025-07-21 21:14:09 +08:00
awkoo
d2ab325e18 优化setup.sh (#288) 2025-07-21 20:23:08 +08:00
ShirkNeko
be393ddb7c manager: Updating susfs binaries 2025-07-21 16:28:50 +08:00
WenHao2130
467d6e0838 ksud: Add KSU_SUKISU to env
This will help module authors determine which root implementation the module is running on.

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-07-20 23:31:29 +08:00
米凛MiRin
46fcf97be3 readme: add chinese translation (#290) 2025-07-20 12:56:03 +05:30
Juno Bủh
72ed1bc4a2 Update Vietnamese Translation (#284) 2025-07-19 17:41:34 +08:00
ShirkNeko
b8544b4f53 manager: Adding a separate status to the update module
fix duplicate installations of update modules due to status confusion. fix #287

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-07-19 16:59:36 +08:00
Cloud_Yun
60f0a721ce script: Simplify repository cloning logic (#283)
Rather than cloning to SukiSU-Ultra and renaming it KernelSU, it would be better to clone it directly as KernelSU.

Signed-off-by: Cloud_Yun <1770669041@qq.com>
2025-07-17 19:37:31 +09:00
ShirkNeko
c62676d643 New Crowdin updates (#280)
* New translations strings.xml (Japanese)

* New translations strings.xml (Russian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Vietnamese)
2025-07-17 14:32:35 +08:00
米凛MiRin
dd2b2e995a docs: fix wrong infomation (#282)
* docs: fix wrong infomation

* docs: fix wrong infomation
2025-07-17 14:32:20 +08:00
MiRinChan
86456a4d95 README & docs: Revise the README and documentation. Thanks to lama's help. 2025-07-16 17:18:31 +05:30
Prslc
11e9e37f43 manager: fix typo in hide_zygisk_implement string and summary (#279)
Signed-off-by: prslc <prslc113@gmail.com>
2025-07-16 18:16:34 +08:00
Prslc
a8a2aef4b5 manager: Display Zygisk implementation method with rezygisk support (#278)
* manager: Display Zygisk implementation method with rezygisk support

Signed-off-by: prslc <prslc113@gmail.com>

* manager: Fix incorrect display of Zygisk implementation when module is disabled

Signed-off-by: prslc <prslc113@gmail.com>

---------

Signed-off-by: prslc <prslc113@gmail.com>
2025-07-16 15:08:20 +08:00
Prslc
77ac0d70fd [skip ci]manager: Add switch to hide Zygisk implementation status information (#277)
Signed-off-by: prslc <prslc113@gmail.com>
2025-07-16 15:08:08 +08:00
wxxsfxyzm
2a00ef96c2 UPSTREAM: manager: improve predictive back animations
This pull request introduces custom screen transition animations to
enhance the overall user experience during navigation.

The key change is the implementation of a custom slide/fade effect for
navigating from main screens (i.e., screens hosted in the bottom
navigation bar) to detail screens. Transitions between the bottom
navigation bar tabs themselves retain a simple, clean cross-fade effect
to ensure a fast and smooth user interaction.

This PR also addresses the root cause of an issue where custom
animations were being overridden by the navigation library's defaults.

During implementation, it was discovered that custom transition
animations defined in the `defaultTransitions` parameter of the
`DestinationsNavHost` in `MainActivity` were not being applied. Instead,
a default fade-in/fade-out animation was always present.

The root cause was traced to the `compose-destinations` KSP (Kotlin
Symbol Processing) code generator. By default, the generator creates a
`NavGraphSpec` (e.g., `RootNavGraph.kt`) that includes its own
`defaultTransitions` property. This property, defined at compile-time
within the generated graph object, has a higher precedence than the
`defaultTransitions` parameter supplied to the `DestinationsNavHost`
composable at runtime.

As a result, our intended custom animations were being ignored and
overridden by the generated default.

To resolve this precedence issue permanently, this PR adopts the
official configuration method recommended by the `compose-destinations`
library.

- The following KSP argument has been added to the
`app/build.gradle.kts` file:

```kotlin
ksp {
    arg("compose-destinations.defaultTransitions", "none")
}
```

- This argument instructs the code generator to omit the
`defaultTransitions` property from the generated `NavGraphSpec`.

- By removing the higher-priority, generated default, the
`defaultTransitions` parameter on `DestinationsNavHost` now functions as
the effective default, allowing our custom animation logic to execute as
intended.

The new animation logic is conditional and defined within
`MainActivity`. It distinguishes between two primary navigation types:

- Main Screen → Detail Screen:

   - Enter: The new detail screen slides in from the right.

   - Exit: The old main screen slides out to the left while fading out.

- Detail Screen → Main Screen (on Pop):

- Pop Enter: The main screen slides back in from the left while fading
in.

   - Pop Exit: The detail screen slides out to the right.

- Between Bottom Navigation Tabs:

- A simple cross-fade (`fadeIn`/`fadeOut`) is maintained for these
transitions to provide a quick and non-disruptive experience when
switching between primary sections of the app.
2025-07-15 09:02:06 +08:00
YangQi0408
39ee1cc41d kernel: core_hook: add support for KernelNoSU (#270)
reorder ksu_handle_prctl checks a bit to allow non-manager to use CMD 15
this allows us to piggyback a small su to KernelSU's permission system after
disabling kernel sucompat

from:
Relax prctl perm check
- 95125c32f9
Allow prctl only for root or manager or su binary
- fa7af67d94
Refine prctl access check, allow /product/bin/su
- dd466dc1b6
Refine prctl check a little bit more
- e7c5b24efa

Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
Co-authored-by: nampud <nampud@users.noreply.github.com>
2025-07-14 15:24:48 +08:00
米凛MiRin
51556d1253 strings: add copyright infomation (#275)
* copyright: Correct copyright owner information

* string: add copyright infomation
2025-07-14 15:21:48 +08:00
米凛MiRin
94df64f234 copyright: Correct copyright owner information (#273) 2025-07-14 00:08:00 +08:00
WenHao2130
02f545b3fb [skip ci] Revert "Redirect the Feature Requests issue template tab (#1788)"
This reverts commit 3695435c3f.

Allow feature requests

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-07-13 10:50:28 +08:00
ShirkNeko
62c7aac75b Merge branch 'main' of https://github.com/SukiSU-Ultra/SukiSU-Ultra 2025-07-12 11:43:23 +08:00
ShirkNeko
755d454960 manager:Add flag bits to module installations to prevent repeated installations
- Modify the time and location of the susfs self-startup scripts
2025-07-12 11:41:58 +08:00
Marat Budkevich
9717fa0de6 fix(manager): add extraspace after "UID:" for Home screen (#268) 2025-07-11 19:18:03 +08:00
ShirkNeko
bd6eb7fddd manager: Update agp version
- Fix bug caused by SuSFS configuration screen returning too quickly
2025-07-11 10:56:30 +08:00
ShirkNeko
9f706873f2 manager: Fixed susfs binary naming scheme 2025-07-09 18:54:27 +08:00
ShirkNeko
c424d5bab4 kernel: simplified signature verification 2025-07-09 12:12:12 +08:00
WenHao2130
e795387c30 manager: Reformat zh-rCN strings
Change-Id: I9880ac9c4e6f74d30110cccfbd774ddfebf89ac0
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-07-09 11:07:54 +08:00
ShirkNeko
044dd9471f manager: Refactor the way information is fetched in the home screen to avoid null pointers
- add an auto-refresh cache feature
2025-07-09 01:33:00 +08:00
YangQi0408
213a15cdb6 manager: 显示 Zygisk 实现方式 (#264)
* manager: Add display of Zygisk Implement feature and formatting code

* manager: Change the method of detecting Zygisk Implement
Signed-off-by: YangQi0408 <a123er3643@163.com>
Co-authored-by: WenHao2130 <WenHao2130@outlook.com>
2025-07-08 22:34:34 +08:00
ShirkNeko
a255ea9d56 [skip ci]New Crowdin updates (#263)
* New translations strings.xml (Russian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Vietnamese)
2025-07-07 17:49:24 +08:00
ShirkNeko
6cbe13dafc Assets: Add susfs executable binary v1.5.9 2025-07-06 14:49:54 +08:00
ShirkNeko
fb5c7c2f9d manager: Updated dependency library version
- Add JNI macros to simplify field setup and object creation
2025-07-06 14:44:36 +08:00
ShirkNeko
b8f2d15bd1 manager: Modify the separator for information cards from a semicolon to a vertical line 2025-07-06 00:56:15 +08:00
ShirkNeko
2bd6929d24 manager: Add the ability to get a list of active managers 2025-07-06 00:26:42 +08:00
ShirkNeko
42b883240e kernel: add multi-manager support 2025-07-05 22:27:39 +08:00
ShirkNeko
4840540038 manager: Modify Dynamic Signature Configuration Input Verification
- Allow sizes beginning with 0x
2025-07-05 17:39:24 +08:00
Juno Bủh
7bfb37a11e [skip ci]Update Vietnamese Translation (#257)
* Update Vietnamese Translation

* Update strings.xml

* Update strings.xml
2025-07-05 16:37:48 +08:00
ShirkNeko
dd6d695020 kernel:Add Dynamic Signature Configuration 2025-07-05 16:21:29 +08:00
ShirkNeko
6a60b72e21 manager: Reorganizing the backup and restore functionality of SuSFS configurations
- Add checking for the existence of a data catalog for applications in SUS Path

- Modify the loading and caching mechanism of the application information class to avoid repeated refreshes,Finish loading with SuperUser.
2025-07-04 17:57:54 +08:00
ShirkNeko
ea3a0cf73b manager: Update the name in the webui description to “SukiSU-Ultra” 2025-07-03 21:09:37 +08:00
ShirkNeko
1816d15ce8 manager: Refactor show SuSFS features
- need to update the kernel
2025-07-03 19:25:52 +08:00
ShirkNeko
79c8f7a709 New Crowdin updates (#247)
* New translations strings.xml (Russian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Japanese)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)
2025-07-03 14:00:45 +08:00
ShirkNeko
b0cc0e6f6d kernel: Update CMD_GET_FULL_VERSION to a new constant value 2025-07-03 00:08:54 +08:00
ShirkNeko
eccc70c0c9 manager: Improve loading and empty state handling in SuperUserScreen 2025-07-03 00:03:59 +08:00
Juno Bủh
fc828ff3aa [skip ci]: Update Vietnamese Translation (#248)
* Update Vietnamese Translation

* Update Vietnamese Translation
2025-07-02 23:34:48 +08:00
ShirkNeko
acb7cfff1b Revert: Rollback some of the changes in "Add option to use WebUI X" and refactor the KsuService
- Solve the problem that SuperUser is not available when opening SU compatibility.
2025-07-02 01:37:37 +08:00
nanLianQing
3729c22dd0 fix: Fix application crashes caused by improper handling of coroutine exceptions (#249) 2025-07-01 23:26:36 +08:00
ShirkNeko
a84cf70730 manager:Fixed the issue of not getting the corresponding app name and icon in SuSFS paths 2025-07-01 20:00:26 +08:00
ShirkNeko
2278fe49d2 manager: Adding optional additions to SUS paths applies functionality corresponding to the package name as well as categorization 2025-07-01 17:29:45 +08:00
ShirkNeko
be14da387e manager: replace Runtime.exec with getRootShell for command execution 2025-07-01 02:48:09 +08:00
ShirkNeko
1fd13d9d8d docs: Updated documentation to introduce SuSFS Configuration Manager and introduction of advanced features 2025-06-30 16:57:56 +08:00
NkBe
4205db6870 [skip ci]Update Traditional Chinese (Taiwan) (#242) 2025-06-30 16:43:32 +08:00
ShirkNeko
70f03081a4 [skip ci]New Crowdin updates (#240)
* New translations strings.xml (Japanese)

* New translations strings.xml (Russian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-06-30 15:51:32 +08:00
Kitty
5ccb779b6a [skip ci] Update Ukrainian translation (#236) 2025-06-30 15:35:34 +08:00
Juno Bủh
b07bc408ce [skip ci] Update Vietnamese Translation (#238)
* Update Vietnamese Translation

* Update Vietnamese Translation

* Update Vietnam
2025-06-30 15:35:12 +08:00
ShirkNeko
7ee1fd63f1 manager: Add Zygote isolation service uninstallation to susfs feature 2025-06-30 15:32:47 +08:00
ShirkNeko
3551441e42 manager: Provides re-editable functionality for all SuSFS path configurations 2025-06-29 20:36:45 +08:00
ShirkNeko
4a1ab76322 manager: Optimize the logical order of Susfs self boot scripts 2025-06-29 19:06:20 +08:00
ShirkNeko
2fedb051b8 manager: Add Hide Bootloader Scripts and Cleanup Tool Residue feature and switch 2025-06-29 17:50:39 +08:00
Juno Bủh
10c35f4baa Update Vietnamese Translation (#229)
* Update Vietnamese Translation

* Update Vietnamese Translation
2025-06-29 12:32:05 +08:00
科家
4f82eda003 manager: Only display slotinformation on AB devices (#233)
还是有人在用 A-Only 分区设备的
2025-06-29 12:31:50 +08:00
Prslc
80f89c0241 [skip ci] manager: Fix unintended LKM dialog when selecting AnyKernel3 install method (#234)
This fixes a logic issue where the KMI selection dialog would still appear
even when the user selected the AnyKernel3 install method.

Signed-off-by: prslc <prslc113@gmail.com>
2025-06-29 12:30:34 +08:00
ShirkNeko
8399f14fad manager: Add SuSFS configuration backup and restore feature
- Optimize susfs self-boot scripts
- Solve some invalid issues where startup duration does not match or is too fast.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-28 23:19:18 +08:00
YC酱luyancib
c49a66d1af Update Kconfig (#224)
make sure kpm config requrement was enabled
2025-06-28 10:19:53 +05:30
Juno Bủh
d66b390361 Update Vietnamese Translation (#223) 2025-06-27 18:47:02 +08:00
ShirkNeko
9c290a8080 [skip ci]: New Crowdin updates (#222)
* New translations strings.xml (Japanese)

* New translations strings.xml (Russian)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-06-27 18:46:45 +08:00
Saksham Singla
48efc28e8f Update Funding.yml (#221)
* Update FUNDING.yml

* add sukisu donation

---------

Co-authored-by: weishu <twsxtd@gmail.com>
2025-06-27 16:55:22 +08:00
ShirkNeko
634adad15c ksud: fmt 2025-06-27 11:15:19 +08:00
古塵
4532bab230 kernel: refactor CMD_GET_FULL_VERSION to safely initialize version string (#220)
Use strscpy()/strlcpy() to populate the version buffer in CMD_GET_FULL_VERSION
instead of relying on uninitialized memory. This ensures the returned string
is null-terminated and avoids exposing garbage data to user space.

Signed-off-by: schqiushui <orochi9999@gmail.com>
2025-06-27 10:42:38 +08:00
ShirkNeko
d3c9b6e739 ksud: handle errors and non compatible ramdisk
Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com>
2025-06-26 14:22:42 +08:00
ShirkNeko
8e4f980db0 ksud: third test properly check if vendor is already patched or not for lkm restoration and also handle magisk patched vendor boot
Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com>
2025-06-26 14:15:49 +08:00
ShirkNeko
cfee357ed1 manager: Optimize susfs management,
- solve some problems caused by new versions not taking effect
2025-06-26 14:10:50 +08:00
ShirkNeko
9393459b27 manager: Improvements to susfs functionality status fetch exceptions
- Show false if unable to fetch
2025-06-26 00:24:33 +08:00
ShirkNeko
60af173a7e manager:Optimize the path configuration to automatically configure the corresponding value as long as the self-boot is enabled 2025-06-25 23:39:04 +08:00
NkBe
23e2377f87 [skip ci]Update README.md (#218)
就单纯怎么看怎么怪,所以手欠改一改
2025-06-25 20:13:06 +08:00
NkBe
d45ba31849 manger: fix lkm detection (#217)
* manger: Rollback crowdin Update (Traditional Chinese Taiwan)

* manger: Try to modify the lkm detection

改了下判斷方式,現在只要核心返回的 version_flags最後一位是 1,is_lkm就會被正確地設成 true
2025-06-25 19:26:54 +08:00
Saksham Singla
c5705c2d5d Bump agp and Gradle to latest (#216)
* Bump AGP

* Bump Gradle

* update gradle to 14.2
2025-06-25 19:15:17 +08:00
ShirkNeko
dfae83cf58 New Crowdin updates (#215)
* New translations strings.xml (Russian)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Indonesian)

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-06-25 16:33:07 +08:00
NkBe
cd5ba3ac3c [skip ci] manger: Update Traditional Chinese (Taiwan) (#214) 2025-06-25 16:26:24 +08:00
ShirkNeko
2c2698f6bc kernel: Rewrite kernle version code management
Co-authored-by: lamadaemon <i@lama.icu>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-24 23:16:16 +08:00
NkBe
f57fe79c5d manager: Update LKM select file prompt, optimize “init_boot / vendor_boot” text (#211)
* Update Install.kt

* Update Install.kt

* Update strings.xml

* Update strings-rCN.xml

* Update strings-rTW.xml

* manger: Add a space to ensure pangu

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-24 15:50:48 +08:00
ShirkNeko
91ae4c9650 manager: Update LKM select file prompt, support “init_boot / vendor_boot” option 2025-06-24 15:03:14 +08:00
ShirkNeko
01f44dc1d9 ksud: Adding LKM patch support for vendor_boot images
- And improve the OTA update mechanism
- Updating the magiskboot library

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-24 12:52:22 +08:00
Prslc
6e35b88041 Kernel: use main branch commit count for consistent versioning (#209) 2025-06-23 18:59:30 +08:00
Re*Index. (ot_inc)
c9c122d79b skip ci :Update & Fix Japanese (#208)
* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update strings.xml

* Update strings.xml

* Update strings.xml
2025-06-23 18:58:03 +08:00
ShirkNeko
4bec5ae7b1 ksud: Update version code calculations to adjust historical version offsets 2025-06-22 18:45:58 +08:00
ShirkNeko
f9b3478dbb Fix text parameter formatting in KstatConfigItemCard, remove redundant “parameter:” prefixes 2025-06-22 18:33:50 +08:00
ShirkNeko
561c82de0a manager: Refactoring the SuSFS configuration layout and simplifying functions 2025-06-22 18:31:11 +08:00
Amicia De Rune
e96ceb84c9 Update Indonesia Translate (#204)
* Update Indonesia Translate

* Update manager/app/src/main/res/values-in/strings.xml

<string name="show_kpm_info">Sembunyikan fungsi KPM</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="show_kpm_info_summary">Sembunyikan fungsi informasi KPM dan menu KPM di bilah navigasi</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="home_ContributionCard_kernelsu">Antusias Untuk SukiSU Ultra</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="home_click_to_ContributionCard_kernelsu">SukiSU Ultra akan menjadi cabang KSU yang relatif independen di masa mendatang, tetapi kami tetap menghargai KernelSU dan MKSU resmi dan sebagainya atas kontribusi mereka!</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="engine_force_ksu">Penggunaan wajib KSU WebUI</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="language_changed">Bahasa dirubah, mulai ulang aplikasi untuk menerapkan</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="icon_switch_summary">Ubah ikon peluncur aplikasi ke ikon KernelSU</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update manager/app/src/main/res/values-in/strings.xml

<string name="show_more_module_info_summary">Pajang info modul tambahan seperti URL pembaruan JSON</string>

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-21 23:40:11 +08:00
ShirkNeko
ddbbeafc64 manager: Added the SUS Mount Hide Control feature added in SuSFS version 1.5.8 2025-06-21 23:39:19 +08:00
ShirkNeko
285478a778 manager: Remove deprecated OverlayFS checks for automatic kernel stats support.
- Updating the LKM build

- New Crowdin updates
2025-06-21 18:34:37 +08:00
Rifat Azad
00ffa86705 kernel/Makefile: check kernelsu driver version from online git repo first, if fails then check local .git and if that also fails then use hardcoded fallback 2025-06-21 17:09:16 +08:00
ShirkNeko
74ec20745c Uniformly use surfaceContainerHigh as the background color of the popup window 2025-06-21 16:34:38 +08:00
ShirkNeko
b7b995bf73 Updating the Crowdin Workflow Configuration 2025-06-21 15:37:33 +08:00
ShirkNeko
29b7f9e0ad New Crowdin updates (#200)
* New translations strings.xml (Japanese)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Chinese Traditional, Hong Kong)

* New translations strings.xml (Russian)
2025-06-21 14:51:13 +08:00
ShirkNeko
00a4c69227 Remove unused strings 2025-06-21 14:18:51 +08:00
愛莉希雅
9c204496c3 完善補充正體中文-香港 (#3) (#194) 2025-06-21 12:41:41 +08:00
ShirkNeko
519401cf39 Fixing SUS Path Execution Errors
- Simplify SuSFS configuration management
2025-06-21 00:21:14 +08:00
愛莉希雅
f69eb5c115 完善補充正體中文-香港#2 (#191) 2025-06-20 21:23:29 +08:00
NkBe
82e96f4394 manger: 完善補充正體中文 (#189)
* manger: Pangu Format

Pangu Format maintained for new text.

* manger: 完善正體中文
2025-06-20 19:11:30 +08:00
Prslc
8e3db00b9b README: Sync README_EN with README (#190) 2025-06-20 19:11:20 +08:00
ShirkNeko
adf299d9f3 manager: Update the feature definition
- remove the unused FEATURE_SUS_OVERLAYFS
- add FEATURE_SUSFS_HAS_MAGIC_MOUNT
2025-06-20 01:52:00 +08:00
rsuntk
483a39c7ac kernel: core_hook: fix refcount leaks on try_umount (#2635)
Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
Signed-off-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
2025-06-19 18:28:21 +08:00
ShirkNeko
c83baad6d5 manager: Add SuSFS Kstat-related configuration 2025-06-19 11:26:15 +08:00
ShirkNeko
2ff3b5ee06 manager: Fix the character processing logic in the clear command of FlashScreen. 2025-06-18 15:52:48 +08:00
ShirkNeko
b537b51034 manager:Add SuSFS to obtain slot uname and build time information 2025-06-18 15:41:08 +08:00
ShirkNeko
bfb6ea3613 manager:Adjust the text display of setting items to allow multi-line text descriptions 2025-06-18 00:12:27 +08:00
ShirkNeko
edf7685e9a strings: Updates the string resources at the current execution location and modifies the associated text display 2025-06-17 23:58:42 +08:00
ShirkNeko
f65f62360a strings: Update path setting related strings and comments 2025-06-17 23:38:37 +08:00
ShirkNeko
af97488d58 manager: Add pseudo kernel and build time execution location settings 2025-06-17 23:30:29 +08:00
ShirkNeko
6b1f73aa3d manager: Small increase in app acquisition speed 2025-06-17 22:34:47 +08:00
Re*Index. (ot_inc)
4eeece9559 Fix Japanese (#186) 2025-06-17 10:52:16 +08:00
ShirkNeko
4d7d5547ac New Crowdin updates (#184)
* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)
2025-06-17 00:56:15 +08:00
NkBe
7b74e70f97 manger: 更新正體中文 (#183)
* manger: 更新正體中文

* 保证 pangu 格式
2025-06-17 00:25:12 +08:00
ShirkNeko
d92f8fc8fd New Crowdin updates (#181)
* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (Danish)

* New translations strings.xml (German)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Italian)

* New translations strings.xml (Japanese)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Russian)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* New translations strings.xml (Thai)

* New translations strings.xml (Croatian)

* New translations strings.xml (Estonian)

* New translations strings.xml (Latvian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

* New translations strings.xml (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Chinese Traditional, Hong Kong)

* New translations strings.xml (Bosnian)

* New translations strings.xml (Kannada)
2025-06-16 21:55:31 +08:00
ShirkNeko
55f9de2fa9 manager: split susfs module creation scripts
- Add a small number of scripts from the susfs4ksu module that lack a weak BL.
2025-06-16 21:50:51 +08:00
ShirkNeko
a12b14ef46 manager: Refactoring get the status of SUSFS functions
- More precise status cases
- Requires a recompile of your kernel to use it.
2025-06-16 20:16:25 +08:00
ShirkNeko
4ce6ff6286 manager: add ability to read and parse /proc/config.gz file, optimize function status checking 2025-06-15 22:48:56 +08:00
ShirkNeko
ce3566640c Add the ability to display more module information, support copying the update configuration address to the clipboard 2025-06-15 22:21:14 +08:00
ShirkNeko
a0a9fb01f4 manager: Add the function of hiding module label rows, optimize the module lagging problem 2025-06-15 21:50:09 +08:00
ShirkNeko
e1bd16d94f manager: Contrasting scroll to top and bottom functions 2025-06-15 20:05:40 +08:00
ShirkNeko
776ae8744c manager: add folder size labels for module items to optimize the display of module information 2025-06-15 19:27:26 +08:00
ShirkNeko
9285945e8b manager: Optimize the animation of floating action buttons, add zoom and transparency animation 2025-06-15 18:47:51 +08:00
ShirkNeko
75e56038ec manager: Add visibility state management for floating action buttons
- Get change button style from Apatch
2025-06-15 18:32:17 +08:00
ShirkNeko
730d58f18b dec++-ify jni part
- Refactoring to c

Co-authored-by: lamadaemon <i@lama.icu>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-15 17:17:19 +08:00
ShirkNeko
67a05e8813 strings: Modifying SuSFS Configuration to Optimize Boot Self-Start Description 2025-06-15 16:05:04 +08:00
ShirkNeko
e95a469bdb manager: Add a default tab item to the AppItem
- Maybe make the spacing consistent? I'm not sure.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-15 15:52:43 +08:00
HSSkyBoy
2ff122e235 manger: 更新正體中文 (#169)
* manger: 更新正體中文

翻譯已更新內容,優化原本翻譯

* Update strings.xml

* 保证 Pangu 格式

* 補全新內容
2025-06-15 15:08:51 +08:00
ShirkNeko
2319452306 manager: Refactor the bootstrap script to automatically create a KSU module to execute instead
- Add Shamiko's Hidden Weakness Bl Scripts

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-15 14:41:14 +08:00
ShirkNeko
a0752d10c7 Merge branch 'main' of https://github.com/SukiSU-Ultra/SukiSU-Ultra 2025-06-15 03:39:13 +08:00
ShirkNeko
9110d89d61 manager: Add configuration for SuSFS logging feature, support enable/disable logging 2025-06-15 03:38:51 +08:00
米凛MiRin
39d6962320 modified: docs/README-en.md (#170)
modified:   docs/README.md
2025-06-15 01:35:25 +08:00
ShirkNeko
7b314116e9 manager: Refactored SuSFS pop-up window and changed to interface to optimize visual experience 2025-06-15 01:32:38 +08:00
ShirkNeko
ef4101cbf9 manager: Optimizing SuSFS Feature Status Display
- Use labels instead of Text

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-14 22:35:36 +08:00
ShirkNeko
85f5459c1d manager: Add support for automatic selection of corresponding SuSFS version and build time artifacts 2025-06-14 22:04:40 +08:00
ShirkNeko
97e367aa92 manager: Update and add SuSFS related settings and functions. 2025-06-14 20:00:16 +08:00
ShirkNeko
7097986cf5 manager: Add loading animation and empty state component
- Improve user experience when app has no content

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-14 15:39:06 +08:00
ShirkNeko
d6c8ef3737 manager: Add configure susfs uname value in more settings
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-14 01:10:40 +08:00
ShirkNeko
d7a5e80d34 manager: Modifying the getHookType function to return a string type
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: rifsxd <rifat.44.azad.rifs@gmail.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-13 15:36:15 +08:00
ShirkNeko
2d9783e3d4 manager: Add the ability to get the hook type 2025-06-13 14:39:16 +08:00
backslashxx
9f407a94e3 kernel: throne_tracker: avoid cross-fs traversal using s_magic check (#2633)
Skip directories that does NOT have the same magic as /data/app.
This is to avoid scanning incfs and any other stacked filesystems.

While this is way dumber, it's way cheaper.
no kern_path(), no missable path_put(), no ref handling.

This supercedes
`throne_tracker: avoid cross fs access
(https://github.com/tiann/KernelSU/pull/2626)`
- upstream
0b6998b474

Signed-off-by: backslashxx
<118538522+backslashxx@users.noreply.github.com>
2025-06-13 02:38:02 +08:00
ShirkNeko
99726a2c4e [skip ci] docs: Updated README file to reflect instructions for the susfs-main branch
kernel: Added sukisu_set_manager_uid function to set the management UID
2025-06-13 02:36:50 +08:00
ShirkNeko
f3675e7f6e Revert "throne_tracker: avoid cross fs access 2025-06-11 18:55:25 +08:00
ShirkNeko
b84d528d99 [skip ci] manager: update values-tr/strings.xml (#161)
Co-authored-by: cvnertnc <148134890+cvnertnc@users.noreply.github.com>
2025-06-11 18:48:08 +08:00
ShirkNeko
0aab0c1d6b manager: Optimize the layout of PullToRefreshBox in SuperUserScreen to remove redundant padding 2025-06-11 00:47:03 +08:00
ShirkNeko
ab2367f7fa manager: Refactor AppItem layout and improve label display 2025-06-11 00:21:13 +08:00
Wang Han
1bac30930f Switch to prepare_creds/commit_creds
Update API as per kernel doc recommends, also fix setup_groups refcount
leak while at it.
2025-06-10 23:39:08 +08:00
ShirkNeko
6a9186300b [skip ci]New Crowdin updates (#156)
* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (Danish)

* New translations strings.xml (German)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Italian)

* New translations strings.xml (Japanese)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Russian)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* New translations strings.xml (Thai)

* New translations strings.xml (Croatian)

* New translations strings.xml (Estonian)

* New translations strings.xml (Latvian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

* New translations strings.xml (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Chinese Traditional, Hong Kong)

* New translations strings.xml (Bosnian)

* New translations strings.xml (Kannada)

* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (Danish)

* New translations strings.xml (German)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Italian)

* New translations strings.xml (Japanese)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Russian)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* New translations strings.xml (Thai)

* New translations strings.xml (Croatian)

* New translations strings.xml (Estonian)

* New translations strings.xml (Latvian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

* New translations strings.xml (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Chinese Traditional, Hong Kong)

* New translations strings.xml (Bosnian)

* New translations strings.xml (Kannada)

* New translations strings.xml (Japanese)

* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)
2025-06-10 23:33:35 +08:00
WenHao2130
e6dea3c29e [skip ci]manager: Drop QQ group invitation link (#158)
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-06-10 23:33:04 +08:00
ShirkNeko
c873ff74cb manager: Add status tracking to ensure that brush-write operations are performed correctly 2025-06-09 02:25:02 +08:00
ShirkNeko
7b6f451cfb manager: Optimize the function of app classification and sorting method 2025-06-09 01:27:33 +08:00
ShirkNeko
73dea0b8e7 Adjusting component spacing and interaction effects 2025-06-08 23:13:41 +08:00
米凛MiRin
f71d617cb3 manager: fix color (#154) 2025-06-08 22:07:29 +08:00
WenHao2130
f0d8e42026 manager: Fix get Google device model issues (#153)
- Inconsistent case of BRAND and MANUFACTURER information on Google devices results in unintended behavior (e.g. Google Google Pixel 9 XL)

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-06-08 21:08:05 +08:00
ShirkNeko
5bbd95e821 manager: Updated superuser interface, added sidebar categories 2025-06-08 20:28:05 +08:00
WenHao2130
fa060dca58 manager: Refactoring the get device name logic (#152)
* manager: Import `getDeviceInfo()` from bmax/APatch

Co-authored-by: GarfieldHan <2652609017@qq.com>
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>

* manager: Use `getDeviceInfo()` instead `Build.DEVICE`

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

---------

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
Co-authored-by: GarfieldHan <2652609017@qq.com>
2025-06-08 15:47:48 +08:00
ShirkNeko
9c7ba5b998 [skip ci]: docs: Updating the README file to add a description of support for non-GKI device builds
完整正體中文本地化

對缺失內容的完整翻譯,對現有內容參考 SukiSU-Ultra 簡體中文優化

New Crowdin updates (#140)

Co-authored-by: HSSkyBoy <HSSkyBoy@outlook.com>
Co-authored-by: cvnertnc <148134890+cvnertnc@users.noreply.github.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-07 22:23:49 +08:00
ShirkNeko
061136900a [skip ci]: New Crowdin updates (#138)
* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (Danish)

* New translations strings.xml (German)

* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (Danish)

* New translations strings.xml (German)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Italian)

* New translations strings.xml (Japanese)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Russian)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* New translations strings.xml (Thai)

* New translations strings.xml (Croatian)

* New translations strings.xml (Estonian)

* New translations strings.xml (Latvian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

* New translations strings.xml (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Chinese Traditional, Hong Kong)

* New translations strings.xml (Bosnian)

* New translations strings.xml (Kannada)
2025-06-03 17:16:15 +08:00
ShirkNeko
6375bf4b7c manager: Simplify state management of components for installation method selection
Remove unnecessary LKM upload logic
2025-06-03 16:57:41 +08:00
ShirkNeko
17288c086a manager: Simplify Home data initialization logic and remove unnecessary caching time 2025-06-03 16:29:41 +08:00
ShirkNeko
15747ceaa5 manager: Simplify KPM display judgment in Home messages 2025-06-03 15:48:21 +08:00
ShirkNeko
675bb20f52 manager: Modify the Show KPM function to hidden and update the related settings. 2025-06-03 15:26:03 +08:00
ShirkNeko
ec0b26a174 manager: Optimize slot selection dialog to remove unnecessary information 2025-06-03 15:15:48 +08:00
ShirkNeko
92f6f2f51e manager: Add card shadow effect control 2025-06-03 14:04:55 +08:00
5ec1cff
587e73b449 manager: persist show system app settings 2025-06-03 13:53:45 +08:00
ShirkNeko
07c9cce4b9 manager: Fixed flickering on activity refresh using a clever method.
- Add Activity lifecycle callback and method to refresh current Activity
2025-06-03 01:43:31 +08:00
ShirkNeko
1d34ea4995 Rename files and update package structure
Add tool classes related to displaying and refreshing data
2025-06-03 01:02:08 +08:00
5ec1cff
d58ec6952c throne_tracker: avoid cross fs access 2025-06-03 00:03:16 +08:00
ShirkNeko
50631aade6 Manager: Refactoring of hidden messages and display of KPM settings status management 2025-06-03 00:00:39 +08:00
ShirkNeko
6df8f6f5d4 refactor: simplify handling and add app restart utility 2025-06-02 22:47:30 +08:00
米凛MiRin
4aee26b48e manager: auto restart after Display KPM Function and Hide other info (#136) 2025-06-02 22:24:17 +08:00
ShirkNeko
3bbe415c7e Merge branch 'main' of https://github.com/SukiSU-Ultra/SukiSU-Ultra 2025-06-02 21:33:55 +08:00
ShirkNeko
892fa9040f docs: add troubleshooting section for KernelSU Manager uninstallation issues 2025-06-02 21:33:48 +08:00
ShirkNeko
cadc123eab [skip ci] New Crowdin updates (#135)
* New translations strings.xml (Romanian)

* New translations strings.xml (Romanian)

* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)
2025-06-02 21:30:13 +08:00
ShirkNeko
3a27537648 [skip ci] strings: update the description of the hidden additional information 2025-06-02 21:06:08 +08:00
ShirkNeko
6fa1a5c8b8 Optimize the logic of badge display in the bottom navigation bar
Add support for settings to hide other information

README: add notice about stuck device solution (#134)

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: =?UTF-8?q?=E7=B1=B3=E5=87=9BMiRin?= <148533509+MiRinChan@users.noreply.github.com>

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-06-02 20:46:49 +08:00
ShirkNeko
b772c8ece1 manager: Refactoring data refresh management
New AppData object to optimize data fetching and state management.
2025-06-02 17:49:34 +08:00
ShirkNeko
c0e839dd8e manager: Implement module count refresh
- update count data periodically and optimize the bottom bar display
2025-06-02 17:24:34 +08:00
ShirkNeko
a6ed7befdc manager: Add count icon to bottom bar
- Remove count icon from Home
- Add put back button for more settings
2025-06-02 16:43:31 +08:00
米凛MiRin
c210b00d54 manager: accessibility optimization (#131)
manager: style optimization
2025-06-02 14:47:59 +08:00
ShirkNeko
13b5290598 [skip ci]New Crowdin updates (#133)
* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (Danish)

* New translations strings.xml (German)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Italian)

* New translations strings.xml (Japanese)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Russian)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Portuguese, Brazilian)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* New translations strings.xml (Thai)

* New translations strings.xml (Croatian)

* New translations strings.xml (Estonian)

* New translations strings.xml (Latvian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

* New translations strings.xml (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Bosnian)

* New translations strings.xml (Kannada)

* New translations strings.xml (Japanese)

* New translations strings.xml (Chinese Simplified)
2025-06-02 14:34:41 +08:00
ShirkNeko
b99516da69 [skip ci]: New Crowdin updates (#130)
* Update source file strings.xml

* New translations strings.xml (Romanian)

* New translations strings.xml (Danish)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Italian)

* New translations strings.xml (Japanese)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Russian)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* New translations strings.xml (Thai)

* New translations strings.xml (Croatian)

* New translations strings.xml (Estonian)

* New translations strings.xml (Latvian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

* New translations strings.xml (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Chinese Traditional, Hong Kong)

* New translations strings.xml (Bosnian)

* New translations strings.xml (Kannada)
2025-06-02 14:03:10 +08:00
lshwjgpt25
fe8b5f2135 [skip ci]: Align superuser app spacing with module spacing (#132) 2025-06-02 13:31:41 +08:00
ShirkNeko
04e1b9bf77 manager: Update back navigation logic in FlashScreen for module flashing 2025-06-02 04:10:41 +08:00
ShirkNeko
b8aaf918fe Merge branch 'main' of https://github.com/SukiSU-Ultra/SukiSU-Ultra 2025-06-02 02:11:40 +08:00
ShirkNeko
54925188e8 Optimize interface element spacing and styles 2025-06-02 02:09:46 +08:00
yycgit1
3443e48ef1 manager: Add alternate app icon toggle (#129)
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
Co-authored-by: MiRinChan <148533509+MiRinChan@users.noreply.github.com>
2025-06-01 22:43:08 +08:00
ShirkNeko
53b3e84890 Optimize button styles in SuperUser 2025-06-01 22:36:23 +08:00
ShirkNeko
a5b85bfdad manager: Update theme color display in MoreSettings screen and adjust title style 2025-06-01 21:56:56 +08:00
ShirkNeko
2817583e3c Optimize icon handling for settings cards
Allow icons to be optional and remove redundant group title displays
2025-06-01 21:24:21 +08:00
ShirkNeko
8a6116b4ec manager: Update surface colors to use cardAlpha for improved theme consistency 2025-06-01 20:58:28 +08:00
cvnertnc
6a4270787a [skip ci]Manager: update values-tr/strings.xml (#126) 2025-06-01 20:29:03 +08:00
ShirkNeko
5457a4772b manager: Refactor card elevation handling and improve theme support 2025-06-01 20:17:22 +08:00
ShirkNeko
ee4c3bb03b manager: Optimize color schemes for themes and module screens
- Remove unnecessary background color settings
2025-06-01 14:04:10 +08:00
ShirkNeko
dd1d17d2cf Optimize the display logic of reboot drop down menu items 2025-06-01 02:27:55 +08:00
WenHao2130
3c353e8f88 manager: Modify the display logic of safe_mode (#125)
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-06-01 01:30:16 +08:00
184 changed files with 23408 additions and 7938 deletions

5
.github/FUNDING.yml vendored
View File

@@ -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

View File

@@ -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."

View 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."

View File

@@ -24,23 +24,23 @@ jobs:
matrix: matrix:
include: include:
- version: "android12-5.10" - version: "android12-5.10"
sub_level: 237
os_patch_level: 2025-06
- version: "android13-5.10"
sub_level: 236 sub_level: 236
os_patch_level: 2025-05 os_patch_level: 2025-05
- version: "android13-5.10"
sub_level: 234
os_patch_level: 2025-03
- 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: 134 sub_level: 138
os_patch_level: 2025-05 os_patch_level: 2025-06
- version: "android15-6.6" - version: "android15-6.6"
sub_level: 87 sub_level: 89
os_patch_level: 2025-05 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:

View File

@@ -4,35 +4,37 @@ on:
push: push:
branches: [ main ] branches: [ main ]
paths: paths:
- 'manager/app/src/main/res/**' - 'manager/app/src/main/res/values/strings.xml'
- 'manager/app/src/main/res/values-*/strings.xml'
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs: jobs:
synchronize-with-crowdin: synchronize-with-crowdin:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: crowdin action - name: Crowdin Action
uses: crowdin/github-action@master uses: crowdin/github-action@v2
with: with:
upload_sources: true upload_sources: true
upload_translations: true upload_translations: true
download_translations: true auto_approve_imported: true
localization_branch_name: "Crowdin" download_translations: true
crowdin_branch_name: "main" skip_untranslated_files: false
create_pull_request: true skip_untranslated_strings: true
pull_request_title: 'New Crowdin Translations'
pull_request_body: 'New Crowdin translations by [Crowdin GH Action](https://github.com/crowdin/github-action)'
pull_request_base_branch_name: 'main'
skip_untranslated_files: true
env:
# A classic GitHub Personal Access Token with the 'repo' scope selected (the user should have write access to the repository).
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
# A numeric ID, found at https://crowdin.com/project/<projectName>/tools/api create_pull_request: true
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} localization_branch_name: "Crowdin"
pull_request_labels: 'enhancement, translation'
pull_request_title: 'opt: sync translation from Crowdin'
# Visit https://crowdin.com/settings#api-key to create this token config: 'crowdin.yml'
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} crowdin_branch_name: "main"
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.idea .idea
.vscode .vscode
.DS_Store

View File

@@ -1,3 +1,6 @@
project_id_env: CROWDIN_PROJECT_ID
api_token_env: CROWDIN_API_TOKEN
preserve_hierarchy: 1
files: files:
- source: /manager/app/src/main/res/values/strings.xml - source: /manager/app/src/main/res/values/strings.xml
translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml

View File

@@ -1,141 +0,0 @@
# SukiSU Ultra
**English** | [简体中文](README.md) | [日本語](README-ja.md) | [Türkçe](README-tr.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
## How to add
Using main branching (non-GKI device builds are not supported) (requires manual integration of susfs)
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
```
Using branches that support non-GKI devices (requires manual integration of susfs)
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
```
## How to use integrated susfs
1. Use the susfs-dev branch directly without any patching
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
## KPM Support
- Based on KernelPatch, we have removed duplicates of KSU and kept only KPM support.
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
We will introduce more APatch-compatible functions to ensure the completeness of KPM functionality.
KPM templates: https://github.com/udochina/KPM-Build-Anywhere
> [!Note]
> 1. `CONFIG_KPM=y` needs to be added.
> 2. Non-GKI devices need to add `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y` as well.
> 3. Some kernel source code below `4.19` also needs to be backport from `4.19` to the header file `set_memory.h`.
## How to do a system update to retain ROOT
- After OTA, don't reboot first, go to the manager flashing/patching kernel interface, find `GKI/non_GKI install` and select the Anykernel3 kernel zip file that needs to be flashed, select the slot that is opposite to the current running slot of the system for flashing, and then reboot to retain the GKI mode update This method is not supported for all non-GKI devices, so please try it yourself. It is the safest way to use TWRP for non-GKI devices.
- Or use LKM mode to install to the unused slot (after OTA).
## Compatibility Status
- KernelSU (versions prior to v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+)
- Older kernels (4.4+) are also compatible, but the kernel must be built manually
- KernelSU can support 3.x kernels (3.4-3.18) through additional reverse ports
- Currently supports `arm64-v8a`, `armeabi-v7a (bare)` and some `X86_64`
## More links
**If you need to submit a translation for the manager go to** https://crowdin.com/project/SukiSU-Ultra
Projects compiled based on Sukisu and susfs
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## Hook method
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
1. **KPROBES hook:**
- Also used for Loadable Kernel Module (LKM)
- Default hook method on GKI kernels.
- Need `CONFIG_KPROBES=y`
2. **Manual hook:**
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
- Default hook method on Non-GKI kernels.
- Need `CONFIG_KSU_MANUAL_HOOK=y`
## Usage
### Universal GKI
Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
> [!Note]
> 1. for devices with GKI 2.0 such as Xiaomi, Redmi, Samsung, etc. (excludes kernel-modified manufacturers such as Meizu, OnePlus, Zenith, and oppo)
> 2. Find the GKI build in [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5). Find the device kernel version. Then download it and use TWRP or kernel flashing tool to flash the zip file with AnyKernel3 suffix.
> 3. The .zip archive without suffix is uncompressed, the gz suffix is the compression used by Tenguet models.
### OnePlus
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
> [!Note]
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
> - Please search for the processor codename by yourself, usually it is all English without numbers.
> - You can find the branch and configuration files from the OnePlus open-source kernel repository.
## Features
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.
- The images of the files `ic_launcher*` with anime character emoticons are copyrighted by [五十根大虾仁](https://space.bilibili.com/370927), the Brand Intellectual Property in the images is owned by [明风OuO](https://space.bilibili.com/274939213), and the vectorization is done by @MiRinChan. Before using these files, in addition to complying with [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt), you also need to comply with the authorization of the two authors to use these artistic contents.
- Except for the files or directories mentioned above, all other parts are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
## 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

View File

@@ -1,137 +0,0 @@
# SukiSU Ultra
**日本語** | [简体中文](README.md) | [English](README-en.md) | [Türkçe](README-tr.md)
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
**試験中なビルドです!自己責任で使用してください!**<br>
このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。
> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。
>
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
## 追加方法
メイン分岐の使用GKI デバイス以外のビルドはサポートされていません。) (手動によるサスフ統合が必要)
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
```
GKI以外のデバイスをサポートするブランチを使用する (手動によるサスフ統合が必要)
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
```
## 統合された susfs の使い方
1. パッチを当てずに susfs-dev ブランチを直接使用してください。
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
## KPM に対応
- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。
- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。
オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
> [!Note]
> 1. `CONFIG_KPM=y` が必要である。
> 2.非 GKI デバイスには `CONFIG_KALLSYMS=y` と `CONFIG_KALLSYMS_ALL=y` も必要です。
> 3.いくつかのカーネル `4.19` およびそれ以降のソースコードでは、 `4.19` からバックポートされた `set_memory.h` ヘッダーファイルも必要です。
## ROOT を保持するシステムアップデートの方法
- OTAの後、最初に再起動せず、マネージャのフラッシュ/パッチカーネルインターフェイスに移動し、`GKI/non_GKI 取り付け`を見つけ、フラッシュする必要があるAnykernel3カーネルzipファイルを選択し、フラッシュするためにシステムの現在の実行スロットと反対のスロットを選択し、GKIモードアップデートを保持するために再起動しますこの方法は、現時点ではすべてのnon_GKIデバイスでサポートされていませんので、各自でお試しください。 (この方法は、すべての非GKIデバイスでサポートされていませんので、ご自身でお試しください)。
- または、LKMモードを使用して未使用のスロットにインストールします(OTA後)。
## 互換性ステータス
- KernelSUv1.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)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## フックの方式
- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。
1. **KPROBES フック:**
- 読み込み可能なカーネルモジュールの場合 (LKM)
- GKI カーネルのデフォルトとなるフック方式
- `CONFIG_KPROBES=y` が必要です
2. **手動でフック:**
- 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5
- 非 GKI カーネル用のデフォルトフッキングメソッド
- `CONFIG_KSU_MANUAL_HOOK=y` が必要です
## 使い方
### ユニバーサルGKI
https://kernelsu.org/zh_CN/guide/installation.html をご参照ください。
> [!Note]
> 1.Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス用 (Meizu、Yiga、Zenith、oppo などのマジックカーネルを搭載したメーカーは除く)。
> 2. [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5) で GKI ビルドを検索します。 デバイスのカーネルバージョンを検索します。 次に、それをダウンロードし、TWRPまたはカーネルフラッシングツールを使用して、AnyKernel3の接尾辞が付いたzipファイルをフラッシュします。
> 接尾辞なしの.zipアーカイブは非圧縮で、接尾辞gzはTenguetモデルで使用されている圧縮方法です。
### OnePlus
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
> [!Note]
> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。
> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。
> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。
## 機能
1. カーネルベースな `su` および root アクセスの管理。
2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) モジュールシステムではなく、 5ec1cff 氏の [Magic Mount](https://github.com/5ec1cff/KernelSU) に基づいています。
3. [アプリプロファイル](https://kernelsu.org/guide/app-profile.html): root 権限をケージ内にロックします。
4. 非 GKI / GKI 1.0 の対応を復活
5. その他のカスタマイズ
6. KPM カーネルモジュールに対応
## ライセンス
- `kernel` ディレクトリ以下のファイルは[GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)です。
- アニメキャラクターの絵文字を含むファイル `ic_launcher*` の画像は[五十根大虾仁](https://space.bilibili.com/370927)が著作権を所有しており、画像内のブランド知的財産権は[明风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) ラオウ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 実装での重要な部分となります。

View File

@@ -1,153 +1,101 @@
<img align='right' src='zakomonochrome-128.svg' width='100px' alt="logo">
# SukiSU Ultra # SukiSU Ultra
**简体中文** | [English](README-en.md) | [日本語](README-ja.md) | [Türkçe](README-tr.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.
**实验性! 使用风险自负!** [![Latest release](https://img.shields.io/github/v/release/SukiSU-Ultra/SukiSU-Ultra?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)
[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/Sukiksu)
[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann) ## Features
>
> 但是,我们将会在未来成为一个单独维护的 KSU 分支
## 如何添加 1. Kernel-based `su` and root access management
2. Module system based on [Magic Mount](https://github.com/5ec1cff/KernelSU)
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
使用 main 分支 (不支持非 GKI 设备构建) (需要手动集成 susfs) - KernelSU (before v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+).
``` - Older kernels (4.4+) are also compatible, but the kernel will have to be built manually.
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
```
使用支持非 GKI 设备的分支 (需要手动集成 susfs) - With more backports, KernelSU can supports 3.x kernel (3.4-3.18).
``` - Currently, only `arm64-v8a`, `armeabi-v7a (bare)` and `X86_64`(some) are supported.
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
```
## 如何集成 susfs ## Installation
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs See [`guide/installation.md`](guide/installation.md)
``` ## Integration
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
## 钩子方法 See [`guide/how-to-integrate.md`](guide/how-to-integrate.md)
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU) ## Translation
1. **KPROBES 钩子:** If you need to submit a translation for the manager, please go to [Crowdin](https://crowdin.com/project/SukiSU-Ultra).
- 用于可加载内核模块 (LKM) ## KPM Support
- GKI 2.0 内核的默认钩子方法
- 需要 `CONFIG_KPROBES=y`
2. **手动钩子:** - Based on KernelPatch, we removed features redundant with KSU and retained only KPM support.
- 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - Work in Progress: Expanding APatch compatibility by integrating additional functions to ensure compatibility across different implementations.
- backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5
- 非 GKI 内核的默认挂钩方法
- 需要 `CONFIG_KSU_MANUAL_HOOK=y`
## KPM 支持 **Open-source repository**: [https://github.com/ShirkNeko/SukiSU_KernelPatch_patch](https://github.com/ShirkNeko/SukiSU_KernelPatch_patch)
- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持 **KPM template**: [https://github.com/udochina/KPM-Build-Anywhere](https://github.com/udochina/KPM-Build-Anywhere)
- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性
开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
> [!Note] > [!Note]
> >
> 1. 需要 `CONFIG_KPM=y` > 1. Requires `CONFIG_KPM=y`
> 2. 非 GKI 设备还需要 `CONFIG_KALLSYMS=y` `CONFIG_KALLSYMS_ALL=y` > 2. Non-GKI devices requires `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y`
> 3. 部分内核 `4.19` 以下源码还需要从 `4.19` 向后移植头文件 `set_memory.h` > 3. For kernels below `4.19`, backporting from `set_memory.h` from `4.19` is required.
## 如何进行系统更新保留 ROOT ## Troubleshooting
- OTA 后先不要重启,进入管理器刷写/修补内核界面,找到 `GKI/non_GKI安装` 选择需要刷写的 Anykernel3 内核压缩文件,选择与现在系统运行槽位相反的槽位进行刷写并重启即可保留 GKI 模式更新(暂不支持所有非 GKI 设备使用这种方法,请自行尝试。非 GKI 设备使用 TWRP 刷写是最稳妥的) 1. Device stuck upon manager app uninstallation?
- 或者使用 LKM 模式的安装到未使用的槽位OTA 后) Uninstall _com.sony.playmemories.mobile_
## 兼容状态 ## Sponsor
- KernelSUv1.0.0 之前版本)正式支持 Android GKI 2.0 设备(内核 5.10+ - [ShirkNeko](https://afdian.com/a/shirkneko) (maintainer of SukiSU)
- [weishu](https://github.com/sponsors/tiann) (author of KernelSU)
- 旧内核4.4+)也兼容,但必须手动构建内核 ## ShirkNeko's sponsorship list
- 通过更多的反向移植KernelSU 可以支持 3.x 内核3.4-3.18 - [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support.
- [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
- 目前支持 `arm64-v8a` `armeabi-v7a (bare)` 和部分 `X86_64` ## 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.
- 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.
**如果你需要为管理器提交翻译请前往** https://crowdin.com/project/SukiSU-Ultra ## Credit
基于 SukiSU 和 susfs 编译的项目 - [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
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) <details>
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) <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.
### 普适的 GKI - [genuine](https://github.com/brevent/genuine/): APK v2 signature validation.
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills.
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html </details>
> [!Note]
>
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用 TWRP 或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
> 3. 一般不带后缀的 .zip 压缩包是未压缩的gz 后缀的为天玑机型所使用的压缩方式
### 一加
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
> [!Note]
>
> - 内核版本只需要填写前两位即可,如 5.105.156.16.6
> - 处理器代号请自行搜索,一般为全英文不带数字的代号
> - 分支和配置文件请自行到一加内核开源地址进行填写
## 特点
1. 基于内核的 `su` 和 root 访问管理
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 内核模块的支持
## 许可证
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
- 有动漫人物图片表情包的这些文件 `ic_launcher*` 的图像版权为[五十根大虾仁](https://space.bilibili.com/370927)所有,图像中的 Brand Intellectual Property 由[明风OuO](https://space.bilibili.com/274939213)所有,矢量化由 @MiRinChan 完成,在使用这些文件之前,除了必须遵守 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) 以外,还需要遵守向前两者索要使用这些艺术内容的授权。
- 除了以上所述的文件或目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
## 爱发电链接
- https://afdian.com/a/shirkneko
## 赞助名单
- [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
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持
## 贡献
- [KernelSU](https://github.com/tiann/KernelSU):原始项目
- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非 GKI 设备重新进行支持
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/)KernelSU 的构想
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
- [genuine](https://github.com/brevent/genuine/)APK v2 签名验证
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分

View 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))

View 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.

View 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
View 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 実装の重要な部分での活用

View File

@@ -1,6 +1,6 @@
# SukiSU Ultra # SukiSU Ultra
**Türkçe** | [简体中文](README.md) | [English](README-en.md) | [日本語](README-ja.md) [English](../README.md) | [简体中文](../zh/README.md) | [日本語](../ja/README.md) | **Türkçe**
[KernelSU](https://github.com/tiann/KernelSU) tabanlı Android cihaz root çözümü [KernelSU](https://github.com/tiann/KernelSU) tabanlı Android cihaz root çözümü
@@ -28,10 +28,10 @@ curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kern
## susfs Nasıl Entegre Edilir ## susfs Nasıl Entegre Edilir
1. Doğrudan susfs-stable veya susfs-dev dalını kullanın, susfs entegrasyonuna gerek yok 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-dev curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
``` ```
## Kanca Yöntemleri ## Kanca Yöntemleri
@@ -121,7 +121,7 @@ Lütfen **tümünü** https://kernelsu.org/zh_CN/guide/installation.html adresin
## Lisans ## Lisans
- `kernel` dizinindeki dosyalar [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır. - `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*` dosyalarının görüntüleri [五十根大虾仁](https://space.bilibili.com/370927) tarafından telif hakkıyla korunmaktadır, görüntülerdeki Marka Fikri Mülkiyeti [明风 OuO](https://space.bilibili.com/274939213)'ye aittir ve vektörleştirme @MiRinChan tarafından yapılmıştır. Bu dosyaları kullanmadan önce, [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) ile uyumlu olmanın yanı sıra, bu sanatsal içerikleri kullanmak için iki yazarın yetkilendirmesine de uymanız gerekir. - 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. - 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ı ## Afdian Bağlantısı
@@ -136,8 +136,6 @@ Lütfen **tümünü** https://kernelsu.org/zh_CN/guide/installation.html adresin
- [yspbwx2010](https://github.com/yspbwx2010) Ç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 - [DARKWWEE](https://github.com/DARKWWEE) 100 USDT için teşekkürler
Eğer yukarıdaki listede adınız yoksa, zamanında güncelleyeceğim, herkese tekrar teşekkür ederim
## Katkıda Bulunanlar ## Katkıda Bulunanlar
- [KernelSU](https://github.com/tiann/KernelSU): Orijinal proje - [KernelSU](https://github.com/tiann/KernelSU): Orijinal proje

View 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
View 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://img.shields.io/github/v/release/SukiSU-Ultra/SukiSU-Ultra?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)
[![频道](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/Sukiksu)
[![协议: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)
[![GitHub 协议](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/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>

View 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但目前无法使用。 -->
将 KernelSUSukiSU添加到内核源代码树的步骤的运行命令将被替换为
### 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) 里找到它)

View 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`...
> - 使用前请确保您了解处理器名称、内核版本等信息。

View 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);
```

View 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

View File

@@ -24,5 +24,34 @@ config KPM
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

View File

@@ -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

View File

@@ -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);
} }

View File

@@ -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);

View File

@@ -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.
@@ -243,11 +244,22 @@ static void nuke_ext4_sysfs() {
} }
ext4_unregister_sysfs(sb); ext4_unregister_sysfs(sb);
path_put(&path);
} }
#else #else
static inline void nuke_ext4_sysfs() { } static inline void nuke_ext4_sysfs() { }
#endif #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,
unsigned long arg4, unsigned long arg5) unsigned long arg4, unsigned long arg5)
{ {
@@ -270,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;
} }
@@ -306,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
@@ -317,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;
@@ -328,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;
} }
@@ -416,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)) {
@@ -436,6 +542,28 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
return 0; 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;
@@ -490,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;
} }
@@ -564,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;
} }
@@ -724,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),

View File

@@ -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
View 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
View 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

View File

@@ -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) {

View File

@@ -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);
@@ -181,4 +181,4 @@ int sukisu_is_kpm_control_code(unsigned long arg2) {
return (arg2 >= CMD_KPM_CONTROL && arg2 <= CMD_KPM_CONTROL_MAX) ? 1 : 0; return (arg2 >= CMD_KPM_CONTROL && arg2 <= CMD_KPM_CONTROL_MAX) ? 1 : 0;
} }
EXPORT_SYMBOL(sukisu_handle_kpm); EXPORT_SYMBOL(sukisu_handle_kpm);

View File

@@ -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();
} }

View File

@@ -23,7 +23,13 @@
#define CMD_UID_SHOULD_UMOUNT 13 #define CMD_UID_SHOULD_UMOUNT 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_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
@@ -35,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
View 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
View 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
View 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);

View File

@@ -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;
@@ -162,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;
} }
@@ -318,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;
} }
@@ -431,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;
} }
@@ -473,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);
@@ -549,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
@@ -560,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
@@ -576,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
@@ -588,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);
@@ -608,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
} }

View File

@@ -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()
@@ -33,4 +38,4 @@ static inline void ksu_invalidate_manager_uid()
ksu_manager_uid = KSU_INVALID_UID; ksu_manager_uid = KSU_INVALID_UID;
} }
#endif #endif

13
kernel/manager_sign.h Normal file
View 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 */

View File

@@ -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
@@ -212,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;
} }
@@ -273,9 +283,9 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
subcmd = data.subcmd; subcmd = data.subcmd;
#endif #endif
rcu_read_lock(); mutex_lock(&ksu_rules);
struct policydb *db = get_policydb(); db = get_policydb();
int ret = -1; int ret = -1;
if (cmd == CMD_NORMAL_PERM) { if (cmd == CMD_NORMAL_PERM) {
@@ -525,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.

View File

@@ -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/SukiSU-Ultra/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.'
} }
@@ -77,4 +76,4 @@ elif [ "$1" = "--cleanup" ]; then
else else
initialize_variables initialize_variables
setup_kernelsu "$@" setup_kernelsu "$@"
fi fi

View File

@@ -22,7 +22,7 @@
extern void escape_to_root(); extern void escape_to_root();
#ifndef CONFIG_KPROBES #ifndef CONFIG_KSU_KPROBES_HOOK
static bool ksu_sucompat_non_kp __read_mostly = true; static bool ksu_sucompat_non_kp __read_mostly = true;
#endif #endif
@@ -54,7 +54,7 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
{ {
const char su[] = SU_PATH; const char su[] = SU_PATH;
#ifndef CONFIG_KPROBES #ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp) { if (!ksu_sucompat_non_kp) {
return 0; return 0;
} }
@@ -81,7 +81,7 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
// const char sh[] = SH_PATH; // const char sh[] = SH_PATH;
const char su[] = SU_PATH; const char su[] = SU_PATH;
#ifndef CONFIG_KPROBES #ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp) { if (!ksu_sucompat_non_kp) {
return 0; return 0;
} }
@@ -130,7 +130,7 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
const char sh[] = KSUD_PATH; const char sh[] = KSUD_PATH;
const char su[] = SU_PATH; const char su[] = SU_PATH;
#ifndef CONFIG_KPROBES #ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp) { if (!ksu_sucompat_non_kp) {
return 0; return 0;
} }
@@ -164,7 +164,7 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
const char su[] = SU_PATH; const char su[] = SU_PATH;
char path[sizeof(su) + 1]; char path[sizeof(su) + 1];
#ifndef CONFIG_KPROBES #ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp){ if (!ksu_sucompat_non_kp){
return 0; return 0;
} }
@@ -189,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);
@@ -222,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)
{ {
@@ -252,16 +311,18 @@ 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 #else
ksu_sucompat_non_kp = true; ksu_sucompat_non_kp = true;
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n"); pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
@@ -270,7 +331,7 @@ void ksu_sucompat_init()
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]);
} }

View File

@@ -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);
ksu_set_manager_uid(np->uid);
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);
}
} 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,9 +222,11 @@ 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);
apk_data->hash = hash; if (apk_data) {
apk_data->exists = true; apk_data->hash = hash;
list_add_tail(&apk_data->list, &apk_path_hash_list); apk_data->exists = true;
list_add_tail(&apk_data->list, &apk_path_hash_list);
}
} }
} }
} }
@@ -214,7 +239,8 @@ 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;
list_for_each_entry(pos, &apk_path_hash_list, list) { list_for_each_entry(pos, &apk_path_hash_list, list) {
@@ -245,6 +271,24 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file)); pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file));
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!
@@ -353,6 +399,16 @@ void track_throne()
break; break;
} }
} }
// 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()) {
@@ -363,6 +419,11 @@ void track_throne()
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: prune:

View File

@@ -1,6 +1,5 @@
@file:Suppress("UnstableApiUsage") @file:Suppress("UnstableApiUsage")
import com.android.build.api.dsl.ApkSigningConfig
import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.android.build.gradle.tasks.PackageAndroidArtifact import com.android.build.gradle.tasks.PackageAndroidArtifact
@@ -107,6 +106,10 @@ android {
} }
} }
ksp {
arg("compose-destinations.defaultTransitions", "none")
}
dependencies { dependencies {
implementation(libs.gson) implementation(libs.gson)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
@@ -159,4 +162,6 @@ dependencies {
implementation(libs.mmrl.webui) implementation(libs.mmrl.webui)
implementation(libs.mmrl.ui) implementation(libs.mmrl.ui)
implementation(libs.accompanist.drawablepainter)
} }

View File

@@ -32,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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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()

View 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
}

View File

@@ -1,313 +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);
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_isKPMEnabled(JNIEnv *env, jobject) {
return is_KPM_enable();
}

View 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
}

View File

@@ -1,105 +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
#define CMD_ENABLE_KPM 100
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;
}
bool is_KPM_enable() {
bool enabled = false;
return ksuctl(CMD_ENABLE_KPM, &enabled, nullptr), enabled;
}

View File

@@ -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,9 +106,17 @@ 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);
@@ -85,4 +124,18 @@ bool is_su_enabled();
bool is_KPM_enable(); bool is_KPM_enable();
#endif //KERNELSU_KSU_H 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

View 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

View File

@@ -1,11 +1,14 @@
package com.sukisu.ultra package com.sukisu.ultra
import android.annotation.SuppressLint 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.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Build 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 com.dergoogler.mmrl.platform.Platform
@@ -14,9 +17,31 @@ import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File import java.io.File
import java.util.Locale 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) { override fun attachBaseContext(base: Context) {
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE) val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
@@ -62,6 +87,9 @@ class KernelSUApplication : Application() {
super.onCreate() super.onCreate()
ksuApp = this ksuApp = this
// 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
Platform.setHiddenApiExemptions() Platform.setHiddenApiExemptions()
val context = this val context = this
@@ -107,4 +135,17 @@ class KernelSUApplication : Application() {
} }
} }
} }
}
// 添加刷新当前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())
}
}
}

View File

@@ -17,6 +17,7 @@ object Natives {
// 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 = 11071 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.
@@ -28,10 +29,38 @@ object Natives {
const val MINIMAL_SUPPORTED_KPM = 12800 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")
} }
@@ -69,6 +98,43 @@ 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 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
@@ -91,9 +157,62 @@ 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)
} }
@Immutable
@Parcelize
@Keep
data class SusfsFeatureStatus(
val statusSusPath: Boolean = false,
val statusSusMount: Boolean = false,
val statusAutoDefaultMount: Boolean = false,
val statusAutoBindMount: Boolean = false,
val statusSusKstat: Boolean = false,
val statusTryUmount: Boolean = false,
val statusAutoTryUmountBind: Boolean = false,
val statusSpoofUname: Boolean = false,
val statusEnableLog: Boolean = false,
val statusHideSymbols: Boolean = false,
val statusSpoofCmdline: Boolean = false,
val statusOpenRedirect: Boolean = false,
val statusMagicMount: Boolean = false,
val statusOverlayfsAutoKstat: Boolean = false,
val statusSusSu: Boolean = false
) : Parcelable
@Immutable
@Parcelize
@Keep
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
@Keep @Keep

View 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()
}
}
}

View File

@@ -1,243 +1,239 @@
package com.sukisu.ultra.ui package com.sukisu.ultra.ui
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.database.ContentObserver
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
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.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.platform.LocalContext import androidx.lifecycle.lifecycleScope
import androidx.compose.ui.res.stringResource
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.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.theme.CardConfig.cardAlpha import zako.zako.zako.zakoui.activity.util.*
import com.sukisu.ultra.ui.util.* import zako.zako.zako.zakoui.activity.component.BottomBar
import androidx.core.content.edit import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.webui.initPlatform
import java.util.Locale
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.lifecycle.lifecycleScope
import com.sukisu.ultra.ui.viewmodel.HomeViewModel import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.webui.initPlatform
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var superUserViewModel: SuperUserViewModel private lateinit var superUserViewModel: SuperUserViewModel
private lateinit var homeViewModel: HomeViewModel private lateinit var homeViewModel: HomeViewModel
internal val settingsStateFlow = MutableStateFlow(SettingsState())
private inner class ThemeChangeContentObserver( data class SettingsState(
handler: Handler, val isHideOtherInfo: Boolean = false,
private val onThemeChanged: () -> Unit val showKpmInfo: Boolean = false
) : ContentObserver(handler) { )
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
onThemeChanged()
}
}
// 应用保存的语言设置 private lateinit var themeChangeObserver: ThemeChangeContentObserver
@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) private var isInitialized = false
Locale.setDefault(locale)
val resources = resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
override fun attachBaseContext(newBase: Context) { override fun attachBaseContext(newBase: Context) {
val prefs = newBase.getSharedPreferences("settings", MODE_PRIVATE) val context = LocaleUtils.applyLocale(newBase)
val languageCode = prefs.getString("app_language", "") ?: ""
var context = newBase
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(newBase.resources.configuration)
config.setLocale(locale)
context = newBase.createConfigurationContext(config)
}
super.attachBaseContext(context) super.attachBaseContext(context)
} }
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// 确保应用正确的语言设置 try {
applyLanguageSetting() // 确保应用正确的语言设置
LocaleUtils.applyLanguageSetting(this)
// 应用自定义 DPI // 应用自定义 DPI
applyCustomDpi() 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)
// 初始化 SuperUserViewModel
superUserViewModel = SuperUserViewModel()
lifecycleScope.launch {
superUserViewModel.fetchAppList()
}
// 初始化 HomeViewModel
homeViewModel = HomeViewModel()
// 预加载数据
lifecycleScope.launch {
homeViewModel.initializeData()
}
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val isFirstRun = prefs.getBoolean("is_first_run", true)
if (isFirstRun) {
ThemeConfig.preventBackgroundRefresh = false
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
} }
prefs.edit { putBoolean("is_first_run", false) }
}
// 加载保存的背景设置 super.onCreate(savedInstanceState)
loadThemeMode()
loadThemeColors()
loadDynamicColorState()
CardConfig.load(applicationContext)
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) { // 使用标记控制初始化流程
runOnUiThread { if (!isInitialized) {
if (!ThemeConfig.preventBackgroundRefresh) { initializeViewModels()
ThemeConfig.backgroundImageLoaded = false initializeData()
loadCustomBackground() isInitialized = true
}
setContent {
KernelSUTheme {
val navController = rememberNavController()
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(
bottomBar = {
AnimatedBottomBar.AnimatedBottomBarWrapper(
showBottomBar = showBottomBar,
content = { BottomBar(navController) }
)
},
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root as NavHostGraphSpec,
navController = navController,
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
// If the target is a detail page (not a bottom navigation page), slide in from the right
if (targetState.destination.route !in bottomBarRoutes) {
slideInHorizontally(initialOffsetX = { it })
} else {
// Otherwise (switching between bottom navigation pages), use fade in
fadeIn(animationSpec = tween(340))
}
}
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
// If navigating from the home page (bottom navigation page) to a detail page, slide out to the left
if (initialState.destination.route in bottomBarRoutes && targetState.destination.route !in bottomBarRoutes) {
slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()
} else {
// Otherwise (switching between bottom navigation pages), use fade out
fadeOut(animationSpec = tween(340))
}
}
override val popEnterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition = {
// If returning to the home page (bottom navigation page), slide in from the left
if (targetState.destination.route in bottomBarRoutes) {
slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()
} else {
// Otherwise (e.g., returning between multiple detail pages), use default fade in
fadeIn(animationSpec = tween(340))
}
}
override val popExitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition = {
// If returning from a detail page (not a bottom navigation page), scale down and fade out
if (initialState.destination.route !in bottomBarRoutes) {
scaleOut(targetScale = 0.9f) + fadeOut()
} else {
// Otherwise, use default fade out
fadeOut(animationSpec = tween(340))
}
}
}
)
}
}
} }
} }
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun initializeViewModels() {
superUserViewModel = SuperUserViewModel()
homeViewModel = HomeViewModel()
// 设置主题变化监听器
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
}
private fun initializeData() {
lifecycleScope.launch {
try {
superUserViewModel.fetchAppList()
} catch (e: Exception) {
e.printStackTrace()
}
} }
contentResolver.registerContentObserver( lifecycleScope.launch {
android.provider.Settings.System.getUriFor("ui_night_mode"), try {
false, homeViewModel.initializeData()
contentObserver } catch (e: Exception) {
) e.printStackTrace()
}
val destroyListeners = mutableListOf<() -> Unit>()
destroyListeners.add {
contentResolver.unregisterContentObserver(contentObserver)
} }
val isManager = Natives.becomeManager(ksuApp.packageName) // 数据刷新协程
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
// 初始化主题相关设置
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
val isManager = AppData.isManager(ksuApp.packageName)
if (isManager) { if (isManager) {
install() install()
UltraToolInstall.tryToInstall() UltraToolInstall.tryToInstall()
} }
}
setContent { override fun onResume() {
KernelSUTheme { try {
val navController = rememberNavController() super.onResume()
val snackBarHostState = remember { SnackbarHostState() } LocaleUtils.applyLanguageSetting(this)
val currentDestination = navController.currentBackStackEntryAsState().value?.destination ThemeUtils.onActivityResume()
val showBottomBar = when (currentDestination?.route) { // 仅在需要时刷新数据
ExecuteModuleActionScreenDestination.route -> false // Hide for ExecuteModuleActionScreen if (isInitialized) {
else -> true refreshData()
}
// pre-init platform to faster start WebUI X activities
LaunchedEffect(Unit) {
initPlatform()
}
Scaffold(
bottomBar = {
AnimatedVisibility(
visible = showBottomBar,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
) {
BottomBar(navController)
}
},
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState
) {
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root as NavHostGraphSpec,
navController = navController,
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
get() = { fadeIn(animationSpec = tween(340)) }
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
get() = { fadeOut(animationSpec = tween(340)) }
}
)
}
}
} }
} catch (e: Exception) {
e.printStackTrace()
} }
} }
// 应用自定义DPI设置 private fun refreshData() {
private fun applyCustomDpi() { lifecycleScope.launch {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try { try {
val resources = resources superUserViewModel.fetchAppList()
val metrics = resources.displayMetrics homeViewModel.initializeData()
metrics.density = customDpi / 160f DataRefreshUtils.refreshData(lifecycleScope)
@Suppress("DEPRECATION")
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -245,125 +241,29 @@ class MainActivity : ComponentActivity() {
} }
override fun onPause() { override fun onPause() {
super.onPause() try {
CardConfig.save(applicationContext) super.onPause()
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit { ThemeUtils.onActivityPause(this)
putBoolean("prevent_background_refresh", true) } catch (e: Exception) {
} e.printStackTrace()
ThemeConfig.preventBackgroundRefresh = true
}
override fun onResume() {
super.onResume()
applyLanguageSetting()
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
loadCustomBackground()
}
lifecycleScope.launch {
superUserViewModel.fetchAppList()
}
lifecycleScope.launch {
homeViewModel.initializeData()
} }
} }
private val destroyListeners = mutableListOf<() -> Unit>()
override fun onDestroy() { override fun onDestroy() {
destroyListeners.forEach { it() } try {
super.onDestroy() ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
super.onDestroy()
} catch (e: Exception) {
e.printStackTrace()
}
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) try {
applyLanguageSetting() super.onConfigurationChanged(newConfig)
} LocaleUtils.applyLanguageSetting(this)
} } catch (e: Exception) {
e.printStackTrace()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName)
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
val kpmVersion = getKpmVersion()
val cardColor = MaterialTheme.colorScheme.surfaceContainer
// 检查是否显示KPM
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
NavigationBar(
modifier = Modifier.windowInsetsPadding(
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
),
containerColor = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
).containerColor,
tonalElevation = cardElevation
) {
BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (!isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
},
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
alwaysShowLabel = false
)
}
} else {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
},
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
alwaysShowLabel = false
)
}
} }
} }
} }

View File

@@ -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(
@@ -122,4 +125,4 @@ private fun AboutCardContent() {
} }
} }
} }
} }

View File

@@ -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()
}
}
}

View File

@@ -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
@@ -138,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)
} }
} }
@@ -186,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) {
"" ""
} }
} }

View File

@@ -63,7 +63,12 @@ fun SearchAppBar(
var onSearch by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) }
// 获取卡片颜色和透明度 // 获取卡片颜色和透明度
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow 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) {

View File

@@ -6,16 +6,19 @@ 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.MaterialTheme
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.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.LabelItem
import com.dergoogler.mmrl.ui.component.text.TextRow import com.dergoogler.mmrl.ui.component.text.TextRow
import com.sukisu.ultra.ui.theme.CardConfig
@Composable @Composable
fun SwitchItem( fun SwitchItem(
@@ -25,63 +28,84 @@ fun SwitchItem(
checked: Boolean, checked: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
beta: Boolean = false, 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) } 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(
modifier = Modifier
.toggleable(
value = checked,
interactionSource = interactionSource,
role = Role.Switch,
enabled = enabled,
indication = LocalIndication.current,
onValueChange = onCheckedChange
),
headlineContent = {
TextRow(
leadingContent = if (beta) {
{
LabelItem(
modifier = Modifier.then(stateAlpha),
text = "Beta"
)
}
} else null
) {
Text(
modifier = Modifier.then(stateAlpha),
text = title,
)
}
},
leadingContent = icon?.let {
{
Icon(
modifier = Modifier.then(stateAlpha),
imageVector = icon,
contentDescription = title
)
}
},
trailingContent = {
Switch(
checked = checked,
enabled = enabled,
onCheckedChange = onCheckedChange,
interactionSource = interactionSource
)
},
supportingContent = {
if (summary != null) {
Text(
modifier = Modifier.then(stateAlpha),
text = summary
)
}
}
)
}
}
@Composable
fun RadioItem(
title: String,
selected: Boolean,
onClick: () -> Unit,
) {
ListItem( ListItem(
modifier = Modifier
.toggleable(
value = checked,
interactionSource = interactionSource,
role = Role.Switch,
enabled = enabled,
indication = LocalIndication.current,
onValueChange = onCheckedChange
),
headlineContent = { headlineContent = {
TextRow( Text(title)
leadingContent = if (beta) {
{
LabelItem(
modifier = Modifier.then(stateAlpha),
text = "Beta"
)
}
} else null
) {
Text(
modifier = Modifier.then(stateAlpha),
text = title,
)
}
}, },
leadingContent = icon?.let { leadingContent = {
{ RadioButton(selected = selected, onClick = onClick)
Icon(
modifier = Modifier.then(stateAlpha),
imageVector = icon,
contentDescription = title,
tint = MaterialTheme.colorScheme.primary
)
}
},
trailingContent = {
Switch(
checked = checked,
enabled = enabled,
onCheckedChange = onCheckedChange,
interactionSource = interactionSource
)
},
supportingContent = {
if (summary != null) {
Text(
modifier = Modifier.then(stateAlpha),
text = summary
)
}
} }
) )
} }

View File

@@ -1,6 +1,5 @@
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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@@ -10,12 +9,10 @@ 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.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 com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.ThemeConfig
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SdStorage import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -32,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
@@ -103,12 +106,12 @@ fun SlotSelectionDialog(
val slotOptions = listOf( val slotOptions = listOf(
ListOption( ListOption(
titleText = stringResource(id = R.string.slot_a), titleText = stringResource(id = R.string.slot_a),
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null, subtitleText = null,
icon = Icons.Filled.SdStorage icon = Icons.Filled.SdStorage
), ),
ListOption( ListOption(
titleText = stringResource(id = R.string.slot_b), titleText = stringResource(id = R.string.slot_b),
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null, subtitleText = null,
icon = Icons.Filled.SdStorage icon = Icons.Filled.SdStorage
) )
) )
@@ -124,19 +127,20 @@ fun SlotSelectionDialog(
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background( .background(
color = if (option.subtitleText != null) { color = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
} else { } else {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
} }
) )
.clickable { .clickable {
onSlotSelected( selectedSlot = when(index) {
when (index) { 0 -> "a"
0 -> "a" else -> "b"
else -> "b" }
}
)
} }
.padding(vertical = 12.dp, horizontal = 16.dp), .padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -144,7 +148,10 @@ fun SlotSelectionDialog(
Icon( Icon(
imageVector = option.icon, imageVector = option.icon,
contentDescription = null, contentDescription = null,
tint = if (option.subtitleText != null) { tint = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.onPrimary MaterialTheme.colorScheme.onPrimary
} else { } else {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
@@ -159,7 +166,10 @@ fun SlotSelectionDialog(
Text( Text(
text = option.titleText, text = option.titleText,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = if (option.subtitleText != null) { color = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.onPrimary MaterialTheme.colorScheme.onPrimary
} else { } else {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
@@ -169,7 +179,10 @@ fun SlotSelectionDialog(
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = if (true) { color = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f) MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
} else { } else {
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
@@ -186,9 +199,10 @@ fun SlotSelectionDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
currentSlot?.let { onSlotSelected(it) } selectedSlot?.let { onSlotSelected(it) }
onDismiss() onDismiss()
} },
enabled = selectedSlot != null
) { ) {
Text( Text(
text = stringResource(android.R.string.ok), text = stringResource(android.R.string.ok),
@@ -221,7 +235,7 @@ data class ListOption(
) )
// Utility function to get current slot // Utility function to get current slot
private fun getCurrentSlot(context: Context): String? { private fun getCurrentSlot(): String? {
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let { 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

View File

@@ -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
)
}
}
}
}

View File

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

View File

@@ -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
)
)
}

View File

@@ -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),

View File

@@ -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,11 +209,10 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
} }
val selection = HashSet(selected) val selection = HashSet(selected)
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
MaterialTheme( MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy( colorScheme = MaterialTheme.colorScheme.copy(
surface = backgroundColor surface = MaterialTheme.colorScheme.surfaceContainerHigh
) )
) { ) {
ListDialog( ListDialog(
@@ -286,11 +288,10 @@ fun CapsPanel(
} }
val selection = HashSet(selected) val selection = HashSet(selected)
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
MaterialTheme( MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy( colorScheme = MaterialTheme.colorScheme.copy(
surface = backgroundColor surface = MaterialTheme.colorScheme.surfaceContainerHigh
) )
) { ) {
ListDialog( ListDialog(
@@ -441,11 +442,10 @@ private fun SELinuxPanel(
) )
) )
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
MaterialTheme( MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy( colorScheme = MaterialTheme.colorScheme.copy(
surface = backgroundColor surface = MaterialTheme.colorScheme.surfaceContainerHigh
) )
) { ) {
InputDialog( InputDialog(

View File

@@ -37,7 +37,6 @@ 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.IconButtonDefaults
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
@@ -60,6 +59,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.shadow 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
@@ -84,6 +84,9 @@ import com.sukisu.ultra.ui.component.profile.AppProfileConfig
import com.sukisu.ultra.ui.component.profile.RootProfileConfig import com.sukisu.ultra.ui.component.profile.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
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
@@ -122,7 +125,12 @@ fun AppProfileScreen(
mutableStateOf(initialProfile) mutableStateOf(initialProfile)
} }
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = CardConfig.cardAlpha val cardAlpha = CardConfig.cardAlpha
Scaffold( Scaffold(
@@ -203,149 +211,173 @@ 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)
Column(modifier = modifier) { MaterialTheme(
ElevatedCard( colorScheme = MaterialTheme.colorScheme.copy(
modifier = Modifier surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
.fillMaxWidth() )
.padding(horizontal = 16.dp, vertical = 8.dp), ) {
shape = MaterialTheme.shapes.medium Column(modifier = modifier) {
) { ElevatedCard(
AppMenuBox(packageName) { modifier = Modifier
ListItem( .fillMaxWidth()
headlineContent = { .padding(horizontal = 16.dp, vertical = 8.dp),
Text( shape = MaterialTheme.shapes.medium,
text = appLabel, colors = cardColors,
style = MaterialTheme.typography.titleMedium elevation = getCardElevation(),
) ) {
}, AppMenuBox(packageName) {
supportingContent = { ListItem(
Text( headlineContent = {
text = packageName, Text(
style = MaterialTheme.typography.bodyMedium, text = appLabel,
color = MaterialTheme.colorScheme.onSurfaceVariant style = MaterialTheme.typography.titleMedium
) )
}, },
leadingContent = appIcon, supportingContent = {
Text(
text = packageName,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
},
leadingContent = appIcon,
)
}
}
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium,
colors = cardColors,
elevation = getCardElevation(),
) {
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(id = R.string.superuser),
checked = isRootGranted,
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },
) )
} }
}
ElevatedCard( Crossfade(
modifier = Modifier targetState = isRootGranted,
.fillMaxWidth() label = "RootAccess"
.padding(horizontal = 16.dp, vertical = 8.dp), ) { current ->
shape = MaterialTheme.shapes.medium Column(
) { modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
SwitchItem( ) {
icon = Icons.Filled.Security, if (current) {
title = stringResource(id = R.string.superuser), val initialMode = if (profile.rootUseDefault) {
checked = isRootGranted, Mode.Default
onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, } else if (profile.rootTemplate != null) {
) Mode.Template
} } else {
Mode.Custom
Crossfade( }
targetState = isRootGranted, var mode by rememberSaveable {
label = "RootAccess" mutableStateOf(initialMode)
) { current ->
Column(
modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)
) {
if (current) {
val initialMode = if (profile.rootUseDefault) {
Mode.Default
} else if (profile.rootTemplate != null) {
Mode.Template
} else {
Mode.Custom
}
var mode by rememberSaveable {
mutableStateOf(initialMode)
}
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium
) {
ProfileBox(mode, true) {
// template mode shouldn't change profile here!
if (it == Mode.Default || it == Mode.Custom) {
onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
}
mode = it
} }
}
AnimatedVisibility(
visible = mode != Mode.Default,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ElevatedCard( ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium,
colors = cardColors,
elevation = getCardElevation(),
) { ) {
Column(modifier = Modifier.padding(vertical = 8.dp)) { ProfileBox(mode, true) {
Crossfade(targetState = mode, label = "ProfileMode") { currentMode -> // template mode shouldn't change profile here!
when (currentMode) { if (it == Mode.Default || it == Mode.Custom) {
Mode.Template -> { onProfileChange(profile.copy(rootUseDefault = it == Mode.Default))
TemplateConfig( }
profile = profile, mode = it
onViewTemplate = onViewTemplate, }
onManageTemplate = onManageTemplate, }
onProfileChange = onProfileChange
) AnimatedVisibility(
visible = mode != Mode.Default,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium,
colors = cardColors,
elevation = getCardElevation(),
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Crossfade(
targetState = mode,
label = "ProfileMode"
) { currentMode ->
when (currentMode) {
Mode.Template -> {
TemplateConfig(
profile = profile,
onViewTemplate = onViewTemplate,
onManageTemplate = onManageTemplate,
onProfileChange = onProfileChange
)
}
Mode.Custom -> {
RootProfileConfig(
fixedName = true,
profile = profile,
onProfileChange = onProfileChange
)
}
else -> {}
} }
Mode.Custom -> {
RootProfileConfig(
fixedName = true,
profile = profile,
onProfileChange = onProfileChange
)
}
else -> {}
} }
} }
} }
} }
} } else {
} 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
) {
ProfileBox(mode, false) {
onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
}
}
AnimatedVisibility(
visible = mode == Mode.Custom,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ElevatedCard( ElevatedCard(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium shape = MaterialTheme.shapes.medium,
colors = cardColors,
elevation = getCardElevation(),
) { ) {
Column(modifier = Modifier.padding(vertical = 8.dp)) { ProfileBox(mode, false) {
AppProfileConfig( onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))
fixedName = true, }
profile = profile, }
enabled = mode == Mode.Custom,
onProfileChange = onProfileChange AnimatedVisibility(
) visible = mode == Mode.Custom,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
shape = MaterialTheme.shapes.medium,
colors = cardColors,
elevation = getCardElevation(),
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
AppProfileConfig(
fixedName = true,
profile = profile,
enabled = mode == Mode.Custom,
onProfileChange = onProfileChange
)
}
} }
} }
} }
@@ -377,12 +409,10 @@ private fun TopBar(
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
) )
Text( Text(
text = packageName, text = packageName,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.alpha(0.8f) modifier = Modifier.alpha(0.8f)
) )
} }
@@ -391,9 +421,6 @@ private fun TopBar(
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = onBack, onClick = onBack,
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSurface
)
) { ) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
@@ -408,7 +435,6 @@ private fun TopBar(
modifier = Modifier.shadow( modifier = Modifier.shadow(
elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f) elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f)
4.dp else 0.dp, 4.dp else 0.dp,
spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
) )
) )
} }
@@ -431,7 +457,6 @@ private fun ProfileBox(
Text( Text(
text = mode.text, text = mode.text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
}, },
leadingContent = { leadingContent = {
@@ -444,7 +469,6 @@ private fun ProfileBox(
HorizontalDivider( HorizontalDivider(
thickness = Dp.Hairline, thickness = Dp.Hairline,
color = MaterialTheme.colorScheme.outlineVariant
) )
ListItem( ListItem(
@@ -574,20 +598,26 @@ private fun AppMenuOption(text: String, onClick: () -> Unit) {
@Composable @Composable
private fun AppProfilePreview() { private fun AppProfilePreview() {
var profile by remember { mutableStateOf(Natives.Profile("")) } var profile by remember { mutableStateOf(Natives.Profile("")) }
Surface { MaterialTheme(
AppProfileInner( colorScheme = MaterialTheme.colorScheme.copy(
packageName = "icu.nullptr.test", surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
appLabel = "Test",
appIcon = {
Icon(
imageVector = Icons.Filled.Android,
contentDescription = null,
)
},
profile = profile,
onProfileChange = {
profile = it
},
) )
) {
Surface {
AppProfileInner(
packageName = "icu.nullptr.test",
appLabel = "Test",
appIcon = {
Icon(
imageVector = Icons.Filled.Android,
contentDescription = null,
)
},
profile = profile,
onProfileChange = {
profile = it
},
)
}
} }
} }

View File

@@ -70,11 +70,15 @@ data class ModuleInstallStatus(
val totalModules: Int = 0, val totalModules: Int = 0,
val currentModule: Int = 0, val currentModule: Int = 0,
val currentModuleName: String = "", val currentModuleName: String = "",
val failedModules: MutableList<String> = mutableListOf() val failedModules: MutableList<String> = mutableListOf(),
val verifiedModules: MutableList<String> = mutableListOf() // 添加已验证模块列表
) )
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus()) 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
} }
@@ -83,7 +87,8 @@ fun updateModuleInstallStatus(
totalModules: Int? = null, totalModules: Int? = null,
currentModule: Int? = null, currentModule: Int? = null,
currentModuleName: String? = null, currentModuleName: String? = null,
failedModule: String? = null failedModule: String? = null,
verifiedModule: String? = null
) { ) {
val current = moduleInstallStatus.value val current = moduleInstallStatus.value
moduleInstallStatus.value = current.copy( moduleInstallStatus.value = current.copy(
@@ -99,6 +104,18 @@ fun updateModuleInstallStatus(
failedModules = updatedFailedModules 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)
@@ -110,6 +127,12 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
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()
@@ -127,18 +150,88 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
// 重置状态 // 重置状态
LaunchedEffect(flashIt) { LaunchedEffect(flashIt) {
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex == 0) { when (flashIt) {
moduleInstallStatus.value = ModuleInstallStatus( is FlashIt.FlashModules -> {
totalModules = flashIt.uris.size, if (flashIt.currentIndex == 0) {
currentModule = 1 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(Unit) { // 处理更新模块安装
if (text.isNotEmpty()) { LaunchedEffect(flashIt) {
if (flashIt !is FlashIt.FlashModuleUpdate) return@LaunchedEffect
if (hasUpdateExecuted || hasUpdateCompleted || text.isNotEmpty()) {
return@LaunchedEffect return@LaunchedEffect
} }
hasUpdateExecuted = true
withContext(Dispatchers.IO) {
setFlashingStatus(FlashingStatus.FLASHING)
try {
logContent.append(text).append("\n")
} catch (_: Exception) {
logContent.append(text).append("\n")
}
flashModuleUpdate(flashIt.uri, onFinish = { showReboot, code ->
if (code != 0) {
text += "$errorCodeString $code.\n$checkLogString\n"
setFlashingStatus(FlashingStatus.FAILED)
} else {
setFlashingStatus(FlashingStatus.SUCCESS)
// 处理模块更新成功后的验证标志
val isVerified = moduleVerificationMap[flashIt.uri] ?: false
ModuleOperationUtils.handleModuleUpdate(context, flashIt.uri, isVerified)
viewModel.markNeedRefresh()
}
if (showReboot) {
text += "\n\n\n"
showFloatAction = true
}
hasUpdateCompleted = true
}, onStdout = {
tempText = "$it\n"
if (tempText.startsWith("")) { // clear command
text = tempText.substring(6)
} else {
text += tempText
}
logContent.append(it).append("\n")
}, onStderr = {
logContent.append(it).append("\n")
})
}
}
// 安装但排除更新模块
LaunchedEffect(flashIt) {
if (flashIt is FlashIt.FlashModuleUpdate) return@LaunchedEffect
if (hasExecuted || hasFlashCompleted || text.isNotEmpty()) {
return@LaunchedEffect
}
hasExecuted = true
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
setFlashingStatus(FlashingStatus.FLASHING) setFlashingStatus(FlashingStatus.FLASHING)
@@ -157,7 +250,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} }
} }
flashIt(context, flashIt, onFinish = { showReboot, code -> flashIt(flashIt, onFinish = { showReboot, code ->
if (code != 0) { if (code != 0) {
text += "$errorCodeString $code.\n$checkLogString\n" text += "$errorCodeString $code.\n$checkLogString\n"
setFlashingStatus(FlashingStatus.FAILED) setFlashingStatus(FlashingStatus.FAILED)
@@ -169,6 +262,28 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} }
} else { } else {
setFlashingStatus(FlashingStatus.SUCCESS) 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() viewModel.markNeedRefresh()
} }
if (showReboot) { if (showReboot) {
@@ -176,6 +291,8 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
showFloatAction = true showFloatAction = true
} }
hasFlashCompleted = true
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) { if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
val nextFlashIt = flashIt.copy( val nextFlashIt = flashIt.copy(
currentIndex = flashIt.currentIndex + 1 currentIndex = flashIt.currentIndex + 1
@@ -187,7 +304,7 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} }
}, onStdout = { }, onStdout = {
tempText = "$it\n" tempText = "$it\n"
if (tempText.startsWith("[H[J")) { // clear command if (tempText.startsWith("")) { // clear command
text = tempText.substring(6) text = tempText.substring(6)
} else { } else {
text += tempText text += tempText
@@ -200,14 +317,20 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
} }
val onBack: () -> Unit = { val onBack: () -> Unit = {
if (currentFlashingStatus.value != FlashingStatus.FLASHING) { val canGoBack = when (flashIt) {
if (flashIt is FlashIt.FlashBoot) { is FlashIt.FlashModuleUpdate -> currentFlashingStatus.value != FlashingStatus.FLASHING
navigator.popBackStack() else -> currentFlashingStatus.value != FlashingStatus.FLASHING
}
if (canGoBack) {
if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.navigate(ModuleScreenDestination)
} else { } else {
viewModel.markNeedRefresh() viewModel.markNeedRefresh()
viewModel.fetchModuleList() viewModel.fetchModuleList()
navigator.navigate(ModuleScreenDestination) { navigator.popBackStack()
}
} }
} }
} }
@@ -221,8 +344,6 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
TopBar( TopBar(
currentFlashingStatus.value, currentFlashingStatus.value,
currentStatus, currentStatus,
navigator = navigator,
flashIt = flashIt,
onBack = onBack, onBack = onBack,
onSave = { onSave = {
scope.launch { scope.launch {
@@ -351,7 +472,7 @@ fun ModuleInstallProgressBar(
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
) { ) {
Text( Text(
text = if (currentModuleName.isNotEmpty()) currentModuleName else stringResource(R.string.module), text = currentModuleName.ifEmpty { stringResource(R.string.module) },
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@@ -433,13 +554,16 @@ fun ModuleInstallProgressBar(
private fun TopBar( private fun TopBar(
status: FlashingStatus, status: FlashingStatus,
moduleStatus: ModuleInstallStatus = ModuleInstallStatus(), moduleStatus: ModuleInstallStatus = ModuleInstallStatus(),
navigator: DestinationsNavigator,
flashIt: FlashIt,
onBack: () -> Unit, onBack: () -> Unit,
onSave: () -> Unit = {}, onSave: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = CardConfig.cardAlpha val cardAlpha = CardConfig.cardAlpha
val statusColor = when(status) { val statusColor = when(status) {
@@ -520,12 +644,22 @@ 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 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(
context: android.content.Context,
flashIt: FlashIt, flashIt: FlashIt,
onFinish: (Boolean, Int) -> Unit, onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit, onStdout: (String) -> Unit,
@@ -552,6 +686,9 @@ fun flashIt(
flashModule(currentUri, onFinish, onStdout, onStderr) 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)
} }

View File

@@ -29,21 +29,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Android import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Memory
import androidx.compose.material.icons.filled.PhoneAndroid
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Security
import androidx.compose.material.icons.filled.SettingsSuggest
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.TaskAlt import androidx.compose.material.icons.outlined.TaskAlt
import androidx.compose.material.icons.outlined.Warning import androidx.compose.material.icons.outlined.Warning
@@ -63,7 +54,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
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.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -75,12 +65,8 @@ import androidx.compose.runtime.rememberCoroutineScope
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.composed
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color 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.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -91,6 +77,7 @@ 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.generated.destinations.InstallScreenDestination import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.generated.destinations.SuSFSConfigScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.KernelVersion import com.sukisu.ultra.KernelVersion
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
@@ -98,15 +85,15 @@ import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.checkNewVersion import com.sukisu.ultra.ui.util.checkNewVersion
import com.sukisu.ultra.ui.util.getKpmModuleCount
import com.sukisu.ultra.ui.util.getKpmVersion
import com.sukisu.ultra.ui.util.getModuleCount
import com.sukisu.ultra.ui.util.getSuperuserCount
import com.sukisu.ultra.ui.util.module.LatestVersionInfo import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import com.sukisu.ultra.ui.util.reboot import com.sukisu.ultra.ui.util.reboot
import com.sukisu.ultra.ui.util.getSuSFS
import com.sukisu.ultra.ui.util.SuSFSManager
import com.sukisu.ultra.ui.viewmodel.HomeViewModel import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -143,9 +130,8 @@ fun HomeScreen(navigator: DestinationsNavigator) {
Scaffold( Scaffold(
topBar = { topBar = {
TopBar( TopBar(
kernelVersion = viewModel.systemStatus.kernelVersion, scrollBehavior = scrollBehavior,
onInstallClick = { navigator.navigate(InstallScreenDestination) }, navigator = navigator
scrollBehavior = scrollBehavior
) )
}, },
contentWindowInsets = WindowInsets.safeDrawing.only( contentWindowInsets = WindowInsets.safeDrawing.only(
@@ -184,8 +170,8 @@ fun HomeScreen(navigator: DestinationsNavigator) {
if (viewModel.systemStatus.requireNewKernel) { if (viewModel.systemStatus.requireNewKernel) {
WarningCard( WarningCard(
stringResource(id = R.string.require_kernel_version).format( stringResource(id = R.string.require_kernel_version).format(
viewModel.systemStatus.ksuVersion, Natives.getSimpleVersionFull(),
Natives.MINIMAL_SUPPORTED_KERNEL Natives.MINIMAL_SUPPORTED_KERNEL_FULL
) )
) )
} }
@@ -205,9 +191,8 @@ fun HomeScreen(navigator: DestinationsNavigator) {
InfoCard( InfoCard(
systemInfo = viewModel.systemInfo, systemInfo = viewModel.systemInfo,
isSimpleMode = viewModel.isSimpleMode, isSimpleMode = viewModel.isSimpleMode,
isHideVersion = viewModel.isHideVersion,
isHideOtherInfo = viewModel.isHideOtherInfo,
isHideSusfsStatus = viewModel.isHideSusfsStatus, isHideSusfsStatus = viewModel.isHideSusfsStatus,
isHideZygiskImplement = viewModel.isHideZygiskImplement,
showKpmInfo = viewModel.showKpmInfo, showKpmInfo = viewModel.showKpmInfo,
lkmMode = viewModel.systemStatus.lkmMode, lkmMode = viewModel.systemStatus.lkmMode,
) )
@@ -257,7 +242,7 @@ fun UpdateCard() {
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
WarningCard( WarningCard(
message = stringResource(id = R.string.new_version_available).format(newVersionCode), message = stringResource(id = R.string.new_version_available).format(newVersionCode),
color = MaterialTheme.colorScheme.surfaceVariant, color = MaterialTheme.colorScheme.outlineVariant,
onClick = { onClick = {
if (changelog.isEmpty()) { if (changelog.isEmpty()) {
uriHandler.openUri(newVersionUrl) uriHandler.openUri(newVersionUrl)
@@ -278,25 +263,22 @@ fun UpdateCard() {
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(id)) }, text = { Text(stringResource(id)) },
onClick = { reboot(reason) }, onClick = { reboot(reason) })
leadingIcon = {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = null,
)
}
)
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TopBar( private fun TopBar(
kernelVersion: KernelVersion, scrollBehavior: TopAppBarScrollBehavior? = null,
onInstallClick: () -> Unit, navigator: DestinationsNavigator
scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow val context = LocalContext.current
val cardAlpha = CardConfig.cardAlpha val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
TopAppBar( TopAppBar(
title = { title = {
@@ -310,13 +292,26 @@ private fun TopBar(
scrolledContainerColor = cardColor.copy(alpha = cardAlpha) scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
), ),
actions = { actions = {
// SuSFS 配置按钮
if (getSuSFS() == "Supported" && SuSFSManager.isBinaryAvailable(context)) {
IconButton(onClick = {
navigator.navigate(SuSFSConfigScreenDestination)
}) {
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = stringResource(R.string.susfs_config_setting_title)
)
}
}
// 重启按钮
var showDropdown by remember { mutableStateOf(false) } var showDropdown by remember { mutableStateOf(false) }
KsuIsValid { KsuIsValid {
IconButton(onClick = { IconButton(onClick = {
showDropdown = true showDropdown = true
}) { }) {
Icon( Icon(
imageVector = Icons.Filled.Refresh, imageVector = Icons.Filled.PowerSettingsNew,
contentDescription = stringResource(id = R.string.reboot) contentDescription = stringResource(id = R.string.reboot)
) )
@@ -325,7 +320,8 @@ private fun TopBar(
}) { }) {
RebootDropdownItem(id = R.string.reboot) RebootDropdownItem(id = R.string.reboot)
val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? val pm =
LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
@@ -349,16 +345,11 @@ private fun StatusCard(
onClickInstall: () -> Unit = {} onClickInstall: () -> Unit = {}
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), colors = getCardColors(
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), if (systemStatus.ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer
modifier = Modifier else MaterialTheme.colorScheme.errorContainer
.fillMaxWidth() ),
.clip(MaterialTheme.shapes.large) elevation = getCardElevation(),
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -375,9 +366,14 @@ private fun StatusCard(
systemStatus.ksuVersion != null -> { systemStatus.ksuVersion != null -> {
val workingModeText = when { val workingModeText = when {
Natives.isSafeMode == true -> stringResource(id = R.string.safe_mode)
else -> stringResource(id = R.string.home_working)
}
val workingModeSurfaceText = when {
systemStatus.lkmMode == true -> "LKM" systemStatus.lkmMode == true -> "LKM"
systemStatus.lkmMode == null && systemStatus.kernelVersion.isGKI1() -> "GKI-1.0" systemStatus.lkmMode == null && systemStatus.kernelVersion.isGKI1() -> "GKI 1.0"
systemStatus.lkmMode == false || systemStatus.kernelVersion.isGKI() -> "GKI-2.0" systemStatus.lkmMode == false || systemStatus.kernelVersion.isGKI() -> "GKI 2.0"
else -> "N-GKI" else -> "N-GKI"
} }
@@ -385,7 +381,11 @@ private fun StatusCard(
Icons.Outlined.TaskAlt, Icons.Outlined.TaskAlt,
contentDescription = stringResource(R.string.home_working), contentDescription = stringResource(R.string.home_working),
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp) modifier = Modifier
.size(28.dp)
.padding(
horizontal = 4.dp
),
) )
Column(Modifier.padding(start = 20.dp)) { Column(Modifier.padding(start = 20.dp)) {
@@ -394,9 +394,9 @@ private fun StatusCard(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text( Text(
text = stringResource(id = R.string.home_working), text = workingModeText,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.primary,
) )
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
@@ -408,76 +408,48 @@ private fun StatusCard(
modifier = Modifier modifier = Modifier
) { ) {
Text( Text(
text = workingModeText, text = workingModeSurfaceText,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSecondary, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) color = MaterialTheme.colorScheme.onPrimary
) )
} }
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(6.dp))
// 机器架构标签或者安全模式标签 // 架构标签
val labelText = if (Natives.isSafeMode) { if (Os.uname().machine != "aarch64") {
stringResource(id = R.string.safe_mode) Surface(
} else { shape = RoundedCornerShape(4.dp),
Os.uname().machine color = MaterialTheme.colorScheme.primary,
} modifier = Modifier
) {
Surface( Text(
shape = RoundedCornerShape(4.dp), text = Os.uname().machine,
color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium,
modifier = Modifier modifier = Modifier.padding(
) { horizontal = 6.dp,
Text( vertical = 2.dp
text = labelText, ),
style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onPrimary
color = MaterialTheme.colorScheme.onSecondary, )
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) }
)
} }
} }
val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) val isHideVersion = LocalContext.current.getSharedPreferences(
"settings",
Context.MODE_PRIVATE
)
.getBoolean("is_hide_version", false) .getBoolean("is_hide_version", false)
val isHideOtherInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("is_hide_other_info", false)
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
if (!isHideVersion) { if (!isHideVersion) {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( systemStatus.ksuFullVersion?.let {
text = stringResource(R.string.home_working_version, systemStatus.ksuVersion),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (!isHideOtherInfo) {
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_superuser_count, getSuperuserCount()),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_module_count, getModuleCount()),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
val kpmVersion = getKpmVersion()
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_kpm_module, getKpmModuleCount()), text = stringResource(R.string.home_working_version, it),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.secondary,
) )
} }
} }
@@ -489,7 +461,11 @@ private fun StatusCard(
Icons.Outlined.Warning, Icons.Outlined.Warning,
contentDescription = stringResource(R.string.home_not_installed), contentDescription = stringResource(R.string.home_not_installed),
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp) modifier = Modifier
.size(28.dp)
.padding(
horizontal = 4.dp
),
) )
Column(Modifier.padding(start = 20.dp)) { Column(Modifier.padding(start = 20.dp)) {
@@ -503,7 +479,7 @@ private fun StatusCard(
Text( Text(
text = stringResource(R.string.home_click_to_install), text = stringResource(R.string.home_click_to_install),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onErrorContainer
) )
} }
} }
@@ -513,7 +489,11 @@ private fun StatusCard(
Icons.Outlined.Block, Icons.Outlined.Block,
contentDescription = stringResource(R.string.home_unsupported), contentDescription = stringResource(R.string.home_unsupported),
tint = MaterialTheme.colorScheme.error, tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(24.dp) modifier = Modifier
.size(28.dp)
.padding(
horizontal = 4.dp
),
) )
Column(Modifier.padding(start = 20.dp)) { Column(Modifier.padding(start = 20.dp)) {
@@ -527,7 +507,7 @@ private fun StatusCard(
Text( Text(
text = stringResource(R.string.home_unsupported_reason), text = stringResource(R.string.home_unsupported_reason),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onErrorContainer
) )
} }
} }
@@ -544,14 +524,7 @@ fun WarningCard(
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(color), colors = getCardColors(color),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -563,7 +536,6 @@ fun WarningCard(
Text( Text(
text = message, text = message,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
) )
} }
} }
@@ -576,16 +548,7 @@ fun ContributionCard() {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -601,14 +564,12 @@ fun ContributionCard() {
Text( Text(
text = stringResource(R.string.home_ContributionCard_kernelsu), text = stringResource(R.string.home_ContributionCard_kernelsu),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_click_to_ContributionCard_kernelsu), text = stringResource(R.string.home_click_to_ContributionCard_kernelsu),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
) )
} }
} }
@@ -622,15 +583,7 @@ fun LearnMoreCard() {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -645,14 +598,12 @@ fun LearnMoreCard() {
Text( Text(
text = stringResource(R.string.home_learn_kernelsu), text = stringResource(R.string.home_learn_kernelsu),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_click_to_learn_kernelsu), text = stringResource(R.string.home_click_to_learn_kernelsu),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
) )
} }
} }
@@ -665,15 +616,7 @@ fun DonateCard() {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f)
)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
@@ -688,14 +631,12 @@ fun DonateCard() {
Text( Text(
text = stringResource(R.string.home_support_title), text = stringResource(R.string.home_support_title),
style = MaterialTheme.typography.titleSmall, style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text( Text(
text = stringResource(R.string.home_support_content), text = stringResource(R.string.home_support_content),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f)
) )
} }
} }
@@ -706,23 +647,14 @@ fun DonateCard() {
private fun InfoCard( private fun InfoCard(
systemInfo: HomeViewModel.SystemInfo, systemInfo: HomeViewModel.SystemInfo,
isSimpleMode: Boolean, isSimpleMode: Boolean,
isHideVersion: Boolean,
isHideOtherInfo: Boolean,
isHideSusfsStatus: Boolean, isHideSusfsStatus: Boolean,
isHideZygiskImplement: Boolean,
showKpmInfo: Boolean, showKpmInfo: Boolean,
lkmMode: Boolean? lkmMode: Boolean?
) { ) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.05f)
)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@@ -733,35 +665,36 @@ private fun InfoCard(
fun InfoCardItem( fun InfoCardItem(
label: String, label: String,
content: String, content: String,
icon: ImageVector = Icons.Default.Info icon: ImageVector? = null,
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.Top,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(vertical = 8.dp)
) { ) {
Icon( if (icon != null) {
imageVector = icon, Icon(
contentDescription = label, imageVector = icon,
modifier = Modifier.size(24.dp), contentDescription = label,
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), modifier = Modifier
) .size(28.dp)
.padding(vertical = 4.dp),
)
}
Spacer(modifier = Modifier.width(16.dp)) Spacer(modifier = Modifier.width(16.dp))
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.weight(1f) .weight(1f)
){ ) {
Text( Text(
text = label, text = label,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = content, text = content,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
softWrap = true softWrap = true
) )
} }
@@ -794,17 +727,65 @@ private fun InfoCard(
icon = Icons.Default.SettingsSuggest, icon = Icons.Default.SettingsSuggest,
) )
if (!isSimpleMode &&
(systemInfo.suSFSStatus != "Supported")) {
InfoCardItem(
stringResource(R.string.home_hook_type),
Natives.getHookType(),
icon = Icons.Default.Link
)
}
// 活跃管理器
if (!isSimpleMode && systemInfo.isDynamicSignEnabled && systemInfo.managersList != null) {
val signatureMap = systemInfo.managersList.managers.groupBy { it.signatureIndex }
val managersText = buildString {
signatureMap.toSortedMap().forEach { (signatureIndex, managers) ->
append(managers.joinToString(", ") { "UID: ${it.uid}" })
append(" ")
append(
when (signatureIndex) {
0 -> "(${stringResource(R.string.default_signature)})"
1 -> "(${stringResource(R.string.dynamic_managerature)})"
else -> if (signatureIndex >= 2) "(${
stringResource(
R.string.signature_index,
signatureIndex
)
})" else "(${stringResource(R.string.unknown_signature)})"
}
)
append(" | ")
}
}.trimEnd(' ', '|')
InfoCardItem(
stringResource(R.string.multi_manager_list),
managersText.ifEmpty { stringResource(R.string.no_active_manager) },
icon = Icons.Default.Group,
)
}
InfoCardItem( InfoCardItem(
stringResource(R.string.home_selinux_status), stringResource(R.string.home_selinux_status),
systemInfo.seLinuxStatus, systemInfo.seLinuxStatus,
icon = Icons.Default.Security, icon = Icons.Default.Security,
) )
if (!isHideZygiskImplement && !isSimpleMode && systemInfo.zygiskImplement != "None") {
InfoCardItem(
stringResource(R.string.home_zygisk_implement),
systemInfo.zygiskImplement,
icon = Icons.Default.Adb,
)
}
if (!isSimpleMode) { if (!isSimpleMode) {
if (lkmMode != true) { // 根据showKpmInfo决定是否显示KPM信息
// 根据showKpmInfo决定是否显示KPM信息 if (lkmMode != true && !showKpmInfo) {
if (showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { val displayVersion =
val displayVersion = if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) { if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) {
val statusText = if (Natives.isKPMEnabled()) { val statusText = if (Natives.isKPMEnabled()) {
stringResource(R.string.kernel_patched) stringResource(R.string.kernel_patched)
} else { } else {
@@ -815,41 +796,57 @@ private fun InfoCard(
"${stringResource(R.string.supported)} (${systemInfo.kpmVersion})" "${stringResource(R.string.supported)} (${systemInfo.kpmVersion})"
} }
InfoCardItem( InfoCardItem(
stringResource(R.string.home_kpm_version), stringResource(R.string.home_kpm_version),
displayVersion, displayVersion,
icon = Icons.Default.Archive icon = Icons.Default.Archive
) )
}
} }
} }
if ((!isSimpleMode) && (!isHideSusfsStatus)) { if (!isSimpleMode && !isHideSusfsStatus &&
if (systemInfo.suSFSStatus == "Supported") { systemInfo.suSFSStatus == "Supported" &&
if (systemInfo.suSFSVersion.isNotEmpty()) { systemInfo.suSFSVersion.isNotEmpty()
val isSUS_SU = systemInfo.suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU" ) {
val infoText = buildString {
append(systemInfo.suSFSVersion)
append(if (isSUS_SU) " (${systemInfo.suSFSVariant})" else " (${stringResource(R.string.manual_hook)})")
if (isSUS_SU) {
if (systemInfo.susSUMode.isNotEmpty()) {
append(" ${stringResource(R.string.sus_su_mode)} ${systemInfo.susSUMode}")
}
}
}
InfoCardItem( val infoText = SuSFSInfoText(systemInfo)
stringResource(R.string.home_susfs_version),
infoText, InfoCardItem(
icon = Icons.Default.Storage stringResource(R.string.home_susfs_version),
) infoText,
} icon = Icons.Default.Storage
} )
} }
} }
} }
} }
@SuppressLint("ComposableNaming")
@Composable
private fun SuSFSInfoText(systemInfo: HomeViewModel.SystemInfo): String = buildString {
append(systemInfo.suSFSVersion)
val isSUS_SU = systemInfo.suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
val isKprobesHook = Natives.getHookType() == "Kprobes"
when {
isSUS_SU && isKprobesHook -> {
append(" (${systemInfo.suSFSVariant})")
if (systemInfo.susSUMode.isNotEmpty()) {
append(" ${stringResource(R.string.sus_su_mode)} ${systemInfo.susSUMode}")
}
}
Natives.getHookType() == "Manual" -> {
append(" (${stringResource(R.string.manual_hook)})")
}
else -> {
append(" (${Natives.getHookType()})")
}
}
}
fun getManagerVersion(context: Context): Pair<String, Long> { fun getManagerVersion(context: Context): Pair<String, Long> {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!! val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
@@ -913,12 +910,3 @@ private fun WarningCardPreview() {
onClick = {}) onClick = {})
} }
} }
@SuppressLint("UnnecessaryComposedModifier")
fun Modifier.disableOverscroll(): Modifier = composed {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
this
} else {
this
}
}

View File

@@ -33,7 +33,6 @@ import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -60,6 +59,7 @@ 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.clip
import androidx.compose.ui.draw.shadow 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
@@ -92,6 +92,8 @@ import com.sukisu.ultra.ui.util.isAbDevice
import com.sukisu.ultra.ui.util.isInitBoot import com.sukisu.ultra.ui.util.isInitBoot
import com.sukisu.ultra.ui.util.rootAvailable import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.getKernelVersion import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.getCardElevation
/** /**
* @author ShirkNeko * @author ShirkNeko
@@ -184,28 +186,13 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} }
val onClickNext = { val onClickNext = {
if (isGKI && 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())
@@ -213,7 +200,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
topBar = { topBar = {
TopBar( TopBar(
onBack = { navigator.popBackStack() }, onBack = { navigator.popBackStack() },
onLkmUpload = onLkmUpload,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
}, },
@@ -230,7 +216,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
) { ) {
SelectInstallMethod( SelectInstallMethod(
isGKI = isGKI, isGKI = isGKI,
isAbDevice = isAbDevice,
onSelected = { method -> onSelected = { method ->
if (method is InstallMethod.HorizonKernel && method.uri != null) { if (method is InstallMethod.HorizonKernel && method.uri != null) {
if (isAbDevice) { if (isAbDevice) {
@@ -253,7 +238,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
(lkmSelection as? LkmSelection.LkmUri)?.let { (lkmSelection as? LkmSelection.LkmUri)?.let {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
@@ -279,7 +264,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
if (method.slot != null) { if (method.slot != null) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
@@ -381,7 +366,6 @@ sealed class InstallMethod {
@Composable @Composable
private fun SelectInstallMethod( private fun SelectInstallMethod(
isGKI: Boolean = false, isGKI: Boolean = false,
isAbDevice: Boolean = false,
onSelected: (InstallMethod) -> Unit = {} onSelected: (InstallMethod) -> Unit = {}
) { ) {
val rootAvailable = rootAvailable() val rootAvailable = rootAvailable()
@@ -389,7 +373,11 @@ private fun SelectInstallMethod(
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>(
@@ -468,8 +456,8 @@ private fun SelectInstallMethod(
} }
} }
var LKMExpanded by remember { mutableStateOf(false) } var lkmExpanded by remember { mutableStateOf(false) }
var GKIExpanded by remember { mutableStateOf(false) } var gkiExpanded by remember { mutableStateOf(false) }
Column( Column(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
@@ -478,38 +466,39 @@ private fun SelectInstallMethod(
if (isGKI) { if (isGKI) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 16.dp)
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
ListItem( MaterialTheme(
leadingContent = { colorScheme = MaterialTheme.colorScheme.copy(
Icon( surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
Icons.Filled.AutoFixHigh, )
contentDescription = null, ) {
tint = MaterialTheme.colorScheme.primary ListItem(
) leadingContent = {
}, Icon(
headlineContent = { Icons.Filled.AutoFixHigh,
Text( contentDescription = null,
stringResource(R.string.Lkm_install_methods), tint = MaterialTheme.colorScheme.primary
style = MaterialTheme.typography.titleMedium )
) },
}, headlineContent = {
modifier = Modifier.clickable { Text(
LKMExpanded = !LKMExpanded stringResource(R.string.Lkm_install_methods),
} style = MaterialTheme.typography.titleMedium
) )
},
modifier = Modifier.clickable {
lkmExpanded = !lkmExpanded
}
)
}
AnimatedVisibility( AnimatedVisibility(
visible = LKMExpanded, visible = lkmExpanded,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut() exit = shrinkVertically() + fadeOut()
) { ) {
@@ -584,38 +573,39 @@ private fun SelectInstallMethod(
if (rootAvailable) { if (rootAvailable) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
ListItem( MaterialTheme(
leadingContent = { colorScheme = MaterialTheme.colorScheme.copy(
Icon( surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
Icons.Filled.FileUpload, )
contentDescription = null, ) {
tint = MaterialTheme.colorScheme.primary ListItem(
) leadingContent = {
}, Icon(
headlineContent = { Icons.Filled.FileUpload,
Text( contentDescription = null,
stringResource(R.string.GKI_install_methods), tint = MaterialTheme.colorScheme.primary
style = MaterialTheme.typography.titleMedium )
) },
}, headlineContent = {
modifier = Modifier.clickable { Text(
GKIExpanded = !GKIExpanded stringResource(R.string.GKI_install_methods),
} style = MaterialTheme.typography.titleMedium
) )
},
modifier = Modifier.clickable {
gkiExpanded = !gkiExpanded
}
)
}
AnimatedVisibility( AnimatedVisibility(
visible = GKIExpanded, visible = gkiExpanded,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut() exit = shrinkVertically() + fadeOut()
) { ) {
@@ -702,11 +692,10 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
} }
var selection by remember { mutableStateOf<String?>(null) } var selection by remember { mutableStateOf<String?>(null) }
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
MaterialTheme( MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy( colorScheme = MaterialTheme.colorScheme.copy(
surface = backgroundColor surface = MaterialTheme.colorScheme.surfaceContainerHigh
) )
) { ) {
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
@@ -729,10 +718,14 @@ 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.surfaceContainerLow val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = cardAlpha val cardAlpha = cardAlpha
TopAppBar( TopAppBar(
@@ -765,4 +758,4 @@ private fun TopBar(
@Composable @Composable
fun SelectInstallPreview() { fun SelectInstallPreview() {
InstallScreen(EmptyDestinationsNavigator) InstallScreen(EmptyDestinationsNavigator)
} }

View File

@@ -8,6 +8,7 @@ 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.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -15,7 +16,6 @@ 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.draw.clip
import androidx.compose.ui.draw.shadow
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
@@ -23,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.*
@@ -33,12 +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 android.app.Activity
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import androidx.compose.ui.res.painterResource
/** /**
* KPM 管理界面 * KPM 管理界面
@@ -49,7 +46,6 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
@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
@@ -57,6 +53,9 @@ fun KpmScreen(
val snackBarHost = remember { SnackbarHostState() } val snackBarHost = remember { SnackbarHostState() }
val confirmDialog = rememberConfirmDialog() val confirmDialog = rememberConfirmDialog()
val listState = rememberLazyListState()
val fabVisible by rememberFabVisibilityState(listState)
val moduleConfirmContentMap = viewModel.moduleList.associate { module -> val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
val moduleFileName = module.id val moduleFileName = module.id
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName) module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
@@ -84,18 +83,17 @@ 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)
} }
@@ -112,7 +110,6 @@ fun KpmScreen(
Text( Text(
text = kpmInstallMode, text = kpmInstallMode,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
) )
}, },
text = { text = {
@@ -121,7 +118,6 @@ fun KpmScreen(
Text( Text(
text = stringResource(R.string.kpm_install_mode_description, it), text = stringResource(R.string.kpm_install_mode_description, it),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -146,9 +142,6 @@ fun KpmScreen(
} }
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Download, imageVector = Icons.Filled.Download,
@@ -176,9 +169,6 @@ fun KpmScreen(
} }
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Inventory, imageVector = Icons.Filled.Inventory,
@@ -209,7 +199,6 @@ fun KpmScreen(
} }
} }
}, },
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge shape = MaterialTheme.shapes.extraLarge
) )
} }
@@ -292,38 +281,34 @@ fun KpmScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( AnimatedFab(visible = fabVisible) {
onClick = { FloatingActionButton(
selectPatchLauncher.launch( contentColor = MaterialTheme.colorScheme.onPrimary,
Intent(Intent.ACTION_GET_CONTENT).apply { containerColor = MaterialTheme.colorScheme.primary,
type = "application/octet-stream" onClick = {
} selectPatchLauncher.launch(
) Intent(Intent.ACTION_GET_CONTENT).apply {
}, type = "application/octet-stream"
icon = { }
Icon( )
imageVector = Icons.Filled.Add, },
contentDescription = stringResource(R.string.kpm_install), content = {
) Icon(
}, painter = painterResource(id = R.drawable.package_import),
text = { contentDescription = null
Text( )
text = stringResource(R.string.kpm_install), }
color = MaterialTheme.colorScheme.onPrimaryContainer )
) }
},
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
expanded = true,
)
}, },
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( Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
@@ -348,7 +333,6 @@ fun KpmScreen(
text = stringResource(R.string.kernel_module_notice), text = stringResource(R.string.kernel_module_notice),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
) )
IconButton( IconButton(
@@ -357,9 +341,6 @@ fun KpmScreen(
sharedPreferences.edit { putBoolean("is_notice_closed", true) } sharedPreferences.edit { putBoolean("is_notice_closed", true) }
}, },
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Close, imageVector = Icons.Filled.Close,
@@ -391,12 +372,12 @@ fun KpmScreen(
stringResource(R.string.kpm_empty), stringResource(R.string.kpm_empty),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
} else { } else {
LazyColumn( LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -443,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)
} }
@@ -473,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)
@@ -518,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(
@@ -528,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,
@@ -548,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()
@@ -585,7 +569,6 @@ private fun KpmModuleItem(
Text( Text(
text = stringResource(R.string.kpm_control), text = stringResource(R.string.kpm_control),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
) )
}, },
text = { text = {
@@ -595,20 +578,14 @@ private fun KpmModuleItem(
label = { label = {
Text( Text(
text = stringResource(R.string.kpm_args), text = stringResource(R.string.kpm_args),
color = MaterialTheme.colorScheme.primary
) )
}, },
placeholder = { placeholder = {
Text( Text(
text = module.args, text = module.args,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
) )
}, },
confirmButton = { confirmButton = {
@@ -627,7 +604,6 @@ private fun KpmModuleItem(
) { ) {
Text( Text(
text = stringResource(R.string.confirm), text = stringResource(R.string.confirm),
color = MaterialTheme.colorScheme.primary
) )
} }
}, },
@@ -635,26 +611,16 @@ private fun KpmModuleItem(
TextButton(onClick = { viewModel.hideInputDialog() }) { TextButton(onClick = { viewModel.hideInputDialog() }) {
Text( Text(
text = stringResource(R.string.cancel), text = stringResource(R.string.cancel),
color = MaterialTheme.colorScheme.primary
) )
} }
}, },
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge shape = MaterialTheme.shapes.extraLarge
) )
} }
Card( Card(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation()
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp) modifier = Modifier.padding(20.dp)
@@ -668,7 +634,6 @@ private fun KpmModuleItem(
Text( Text(
text = module.name, text = module.name,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -676,19 +641,16 @@ private fun KpmModuleItem(
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,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
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,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
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,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -698,7 +660,6 @@ private fun KpmModuleItem(
Text( Text(
text = module.description, text = module.description,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
@@ -711,10 +672,6 @@ private fun KpmModuleItem(
onClick = { viewModel.showInputDialog(module.id) }, onClick = { viewModel.showInputDialog(module.id) },
enabled = module.hasAction, enabled = module.hasAction,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Settings, imageVector = Icons.Filled.Settings,
@@ -746,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
} }

View File

@@ -1,6 +1,9 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.annotation.SuppressLint
import android.app.Activity.* import android.app.Activity.*
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
@@ -8,12 +11,22 @@ import android.util.Log
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.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
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.LazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.* import androidx.compose.material.icons.automirrored.outlined.*
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
@@ -23,11 +36,13 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
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.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.painterResource
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.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@@ -68,28 +83,58 @@ import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import androidx.core.content.edit import androidx.core.content.edit
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.webui.WebUIXActivity import com.sukisu.ultra.ui.webui.WebUIXActivity
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import androidx.core.net.toUri import androidx.core.net.toUri
import com.dergoogler.mmrl.platform.model.ModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import com.sukisu.ultra.ui.component.AnimatedFab
import com.sukisu.ultra.ui.component.rememberFabVisibilityState
import com.sukisu.ultra.ui.theme.getCardElevation
// 菜单项数据类
data class ModuleBottomSheetMenuItem(
val icon: ImageVector,
val titleRes: Int,
val onClick: () -> Unit
)
/** /**
* @author ShirkNeko * @author ShirkNeko
* @date 2025/5/31. * @date 2025/5/31.
*/ */
@SuppressLint("ResourceType", "AutoboxingStateCreation")
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@Composable @Composable
fun ModuleScreen(navigator: DestinationsNavigator) { fun ModuleScreen(navigator: DestinationsNavigator) {
val viewModel = viewModel<ModuleViewModel>() val viewModel = viewModel<ModuleViewModel>()
val context = LocalContext.current val context = LocalContext.current
val prefs = context.getSharedPreferences("settings",MODE_PRIVATE)
val snackBarHost = LocalSnackbarHost.current val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val confirmDialog = rememberConfirmDialog() val confirmDialog = rememberConfirmDialog()
var lastClickTime by remember { mutableStateOf(0L) } var lastClickTime by remember { mutableStateOf(0L) }
// 签名验证弹窗状态
var showSignatureDialog by remember { mutableStateOf(false) }
var signatureDialogMessage by remember { mutableStateOf("") }
var isForceVerificationFailed by remember { mutableStateOf(false) }
var pendingInstallAction by remember { mutableStateOf<(() -> Unit)?>(null) }
// 初始化缓存系统
LaunchedEffect(Unit) {
viewModel.initializeCache(context)
}
// BottomSheet状态
val bottomSheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
var showBottomSheet by remember { mutableStateOf(false) }
val listState = rememberLazyListState()
val fabVisible by rememberFabVisibilityState(listState)
val selectZipLauncher = rememberLauncherForActivityResult( val selectZipLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
) { ) {
@@ -137,13 +182,53 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) )
if (confirmResult == ConfirmResult.Confirmed) { if (confirmResult == ConfirmResult.Confirmed) {
try { // 验证模块签名
// 批量安装模块 val forceVerification = prefs.getBoolean("force_signature_verification", false)
navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules))) val verificationResults = mutableMapOf<Uri, Boolean>()
viewModel.markNeedRefresh()
} catch (e: Exception) { for (uri in selectedModules) {
Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}") val isVerified = verifyModuleSignature(context, uri)
snackBarHost.showSnackbar("Error while installing module: ${e.message}") verificationResults[uri] = isVerified
// 存储验证状态
setModuleVerificationStatus(uri, isVerified)
if (forceVerification && !isVerified) {
withContext(Dispatchers.Main) {
signatureDialogMessage = context.getString(R.string.module_signature_invalid_message)
isForceVerificationFailed = true
showSignatureDialog = true
}
return@launch
} else if (!isVerified) {
withContext(Dispatchers.Main) {
signatureDialogMessage = context.getString(R.string.module_signature_verification_failed)
isForceVerificationFailed = false
pendingInstallAction = {
try {
navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules)))
viewModel.markNeedRefresh()
} catch (e: Exception) {
Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}")
scope.launch {
snackBarHost.showSnackbar("Error while installing module: ${e.message}")
}
}
}
showSignatureDialog = true
}
return@launch
}
}
// 所有模块签名验证通过,直接安装
if (verificationResults.all { it.value }) {
try {
navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(selectedModules)))
viewModel.markNeedRefresh()
} catch (e: Exception) {
Log.e("ModuleScreen", "Error navigating to FlashScreen: ${e.message}")
snackBarHost.showSnackbar("Error while installing module: ${e.message}")
}
} }
} }
} else { } else {
@@ -167,6 +252,28 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) )
if (confirmResult == ConfirmResult.Confirmed) { if (confirmResult == ConfirmResult.Confirmed) {
// 验证模块签名
val forceVerification = prefs.getBoolean("force_signature_verification", false)
val isVerified = verifyModuleSignature(context, uri)
// 存储验证状态
setModuleVerificationStatus(uri, isVerified)
if (forceVerification && !isVerified) {
signatureDialogMessage = context.getString(R.string.module_signature_invalid_message)
isForceVerificationFailed = true
showSignatureDialog = true
return@launch
} else if (!isVerified) {
signatureDialogMessage = context.getString(R.string.module_signature_verification_failed)
isForceVerificationFailed = false
pendingInstallAction = {
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
viewModel.markNeedRefresh()
}
showSignatureDialog = true
return@launch
}
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
viewModel.markNeedRefresh() viewModel.markNeedRefresh()
} }
@@ -181,7 +288,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost) val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost)
val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost) val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost)
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) { if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) {
@@ -202,6 +308,34 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
contract = ActivityResultContracts.StartActivityForResult() contract = ActivityResultContracts.StartActivityForResult()
) { viewModel.fetchModuleList() } ) { viewModel.fetchModuleList() }
// BottomSheet菜单项
val bottomSheetMenuItems = remember {
listOf(
ModuleBottomSheetMenuItem(
icon = Icons.Outlined.Save,
titleRes = R.string.backup_modules,
onClick = {
backupLauncher.launch(ModuleModify.createBackupIntent())
scope.launch {
bottomSheetState.hide()
showBottomSheet = false
}
}
),
ModuleBottomSheetMenuItem(
icon = Icons.Outlined.RestoreFromTrash,
titleRes = R.string.restore_modules,
onClick = {
restoreLauncher.launch(ModuleModify.createRestoreIntent())
scope.launch {
bottomSheetState.hide()
showBottomSheet = false
}
}
)
)
}
Scaffold( Scaffold(
topBar = { topBar = {
SearchAppBar( SearchAppBar(
@@ -210,104 +344,23 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
onSearchTextChange = { viewModel.search = it }, onSearchTextChange = { viewModel.search = it },
onClearClick = { viewModel.search = "" }, onClearClick = { viewModel.search = "" },
dropdownContent = { dropdownContent = {
var showDropdown by remember { mutableStateOf(false) }
IconButton( IconButton(
onClick = { showDropdown = true }, onClick = { showBottomSheet = true },
) { ) {
Icon( Icon(
imageVector = Icons.Filled.MoreVert, imageVector = Icons.Filled.MoreVert,
contentDescription = stringResource(id = R.string.settings), contentDescription = stringResource(id = R.string.settings),
) )
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_action_first)) },
trailingIcon = {
Checkbox(
checked = viewModel.sortActionFirst,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
},
onClick = {
viewModel.sortActionFirst = !viewModel.sortActionFirst
prefs.edit {
putBoolean(
"module_sort_action_first",
viewModel.sortActionFirst
)
}
scope.launch {
viewModel.fetchModuleList()
}
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.module_sort_enabled_first)) },
trailingIcon = {
Checkbox(
checked = viewModel.sortEnabledFirst,
onCheckedChange = null,
colors = CheckboxDefaults.colors(
checkedColor = MaterialTheme.colorScheme.primary,
uncheckedColor = MaterialTheme.colorScheme.outline
)
)
},
onClick = {
viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst
prefs.edit {
putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst)
}
scope.launch {
viewModel.fetchModuleList()
}
}
)
HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp))
DropdownMenuItem(
text = { Text(stringResource(R.string.backup_modules)) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Download,
contentDescription = stringResource(R.string.backup),
)
},
onClick = {
showDropdown = false
backupLauncher.launch(ModuleModify.createBackupIntent())
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.restore_modules)) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Refresh,
contentDescription = stringResource(R.string.restore),
)
},
onClick = {
showDropdown = false
restoreLauncher.launch(ModuleModify.createRestoreIntent())
}
)
}
} }
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
) )
}, },
floatingActionButton = { floatingActionButton = {
if (!hideInstallButton) { AnimatedFab(visible = !hideInstallButton && fabVisible) {
val moduleInstall = stringResource(id = R.string.module_install) FloatingActionButton(
ExtendedFloatingActionButton( contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary,
onClick = { onClick = {
selectZipLauncher.launch( selectZipLauncher.launch(
Intent(Intent.ACTION_GET_CONTENT).apply { Intent(Intent.ACTION_GET_CONTENT).apply {
@@ -316,20 +369,12 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
} }
) )
}, },
icon = { content = {
Icon( Icon(
imageVector = Icons.Filled.Add, painter = painterResource(id = R.drawable.package_import),
contentDescription = moduleInstall, contentDescription = null
) )
}, }
text = {
Text(
text = moduleInstall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
},
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
expanded = true,
) )
} }
}, },
@@ -353,7 +398,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
Icon( Icon(
imageVector = Icons.Outlined.Warning, imageVector = Icons.Outlined.Warning,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier modifier = Modifier
.size(64.dp) .size(64.dp)
.padding(bottom = 16.dp) .padding(bottom = 16.dp)
@@ -362,7 +406,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
stringResource(R.string.module_magisk_conflict), stringResource(R.string.module_magisk_conflict),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -371,11 +414,15 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
ModuleList( ModuleList(
navigator = navigator, navigator = navigator,
viewModel = viewModel, viewModel = viewModel,
listState = listState,
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
boxModifier = Modifier.padding(innerPadding), boxModifier = Modifier.padding(innerPadding),
onInstallModule = { onInstallModule = {
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it))) navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it)))
}, },
onUpdateModule = {
navigator.navigate(FlashScreenDestination(FlashIt.FlashModuleUpdate(it)))
},
onClickModule = { id, name, hasWebUi -> onClickModule = { id, name, hasWebUi ->
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < 600) { if (currentTime - lastClickTime < 600) {
@@ -438,6 +485,260 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
) )
} }
} }
// BottomSheet
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = {
showBottomSheet = false
},
sheetState = bottomSheetState,
dragHandle = {
Surface(
modifier = Modifier.padding(vertical = 11.dp),
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
shape = RoundedCornerShape(16.dp)
) {
Box(
Modifier.size(
width = 32.dp,
height = 4.dp
)
)
}
}
) {
ModuleBottomSheetContent(
menuItems = bottomSheetMenuItems,
viewModel = viewModel,
prefs = prefs,
scope = scope,
bottomSheetState = bottomSheetState,
onDismiss = { showBottomSheet = false }
)
}
}
// 签名验证弹窗
if (showSignatureDialog) {
AlertDialog(
onDismissRequest = { showSignatureDialog = false },
icon = {
Icon(
imageVector = Icons.Outlined.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
},
title = {
Text(
text = stringResource(R.string.module_signature_invalid),
color = MaterialTheme.colorScheme.error
)
},
text = {
Text(text = signatureDialogMessage)
},
confirmButton = {
if (isForceVerificationFailed) {
// 强制验证失败,只显示确定按钮
TextButton(
onClick = { showSignatureDialog = false }
) {
Text(stringResource(R.string.confirm))
}
} else {
// 非强制验证失败,显示继续安装按钮
TextButton(
onClick = {
showSignatureDialog = false
pendingInstallAction?.invoke()
pendingInstallAction = null
}
) {
Text(stringResource(R.string.install))
}
}
},
dismissButton = if (!isForceVerificationFailed) {
{
TextButton(
onClick = {
showSignatureDialog = false
pendingInstallAction = null
}
) {
Text(stringResource(R.string.cancel))
}
}
} else {
null
}
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ModuleBottomSheetContent(
menuItems: List<ModuleBottomSheetMenuItem>,
viewModel: ModuleViewModel,
prefs: android.content.SharedPreferences,
scope: kotlinx.coroutines.CoroutineScope,
bottomSheetState: SheetState,
onDismiss: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp)
) {
// 标题
Text(
text = stringResource(R.string.menu_options),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// 菜单选项网格
LazyVerticalGrid(
columns = GridCells.Fixed(4),
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(menuItems) { menuItem ->
ModuleBottomSheetMenuItemView(
menuItem = menuItem
)
}
}
// 排序选项
Spacer(modifier = Modifier.height(24.dp))
HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp))
Text(
text = stringResource(R.string.sort_options),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
)
// 排序选项
Column(
modifier = Modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 优先显示有操作的模块
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.module_sort_action_first),
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = viewModel.sortActionFirst,
onCheckedChange = { checked ->
viewModel.sortActionFirst = checked
prefs.edit {
putBoolean("module_sort_action_first", checked)
}
scope.launch {
viewModel.fetchModuleList()
bottomSheetState.hide()
onDismiss()
}
}
)
}
// 优先显示已启用的模块
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.module_sort_enabled_first),
style = MaterialTheme.typography.bodyMedium
)
Switch(
checked = viewModel.sortEnabledFirst,
onCheckedChange = { checked ->
viewModel.sortEnabledFirst = checked
prefs.edit {
putBoolean("module_sort_enabled_first", checked)
}
scope.launch {
viewModel.fetchModuleList()
bottomSheetState.hide()
onDismiss()
}
}
)
}
}
}
}
@Composable
private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) {
// 添加交互状态
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(
targetValue = if (isPressed) 0.95f else 1.0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessHigh
),
label = "menuItemScale"
)
Column(
modifier = Modifier
.fillMaxWidth()
.scale(scale)
.clickable(
interactionSource = interactionSource,
indication = null
) { menuItem.onClick() }
.padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Surface(
modifier = Modifier.size(48.dp),
shape = CircleShape,
color = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer
) {
Box(
contentAlignment = Alignment.Center
) {
Icon(
imageVector = menuItem.icon,
contentDescription = stringResource(menuItem.titleRes),
modifier = Modifier.size(24.dp)
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(menuItem.titleRes),
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
maxLines = 2
)
} }
} }
@@ -446,9 +747,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
private fun ModuleList( private fun ModuleList(
navigator: DestinationsNavigator, navigator: DestinationsNavigator,
viewModel: ModuleViewModel, viewModel: ModuleViewModel,
listState: LazyListState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
boxModifier: Modifier = Modifier, boxModifier: Modifier = Modifier,
onInstallModule: (Uri) -> Unit, onInstallModule: (Uri) -> Unit,
onUpdateModule: (Uri) -> Unit,
onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit,
context: Context, context: Context,
snackBarHost: SnackbarHostState snackBarHost: SnackbarHostState
@@ -536,7 +839,12 @@ private fun ModuleList(
downloadUrl, downloadUrl,
fileName, fileName,
downloading, downloading,
onDownloaded = onInstallModule, onDownloaded = { uri ->
// 验证更新模块的签名
val isVerified = verifyModuleSignature(context, uri)
setModuleVerificationStatus(uri, isVerified)
onUpdateModule(uri)
},
onDownloading = { onDownloading = {
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show() Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show()
@@ -568,6 +876,8 @@ private fun ModuleList(
val success = loadingDialog.withLoading { val success = loadingDialog.withLoading {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (isUninstall) { if (isUninstall) {
// 卸载时移除验证标志
ModuleOperationUtils.handleModuleUninstall(module.dirId)
uninstallModule(module.dirId) uninstallModule(module.dirId)
} else { } else {
restoreModule(module.dirId) restoreModule(module.dirId)
@@ -577,6 +887,7 @@ private fun ModuleList(
if (success) { if (success) {
viewModel.fetchModuleList() viewModel.fetchModuleList()
viewModel.markNeedRefresh()
} }
if (!isUninstall) return if (!isUninstall) return
val message = if (success) { val message = if (success) {
@@ -606,6 +917,7 @@ private fun ModuleList(
isRefreshing = viewModel.isRefreshing isRefreshing = viewModel.isRefreshing
) { ) {
LazyColumn( LazyColumn(
state = listState,
modifier = modifier, modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = remember { contentPadding = remember {
@@ -640,7 +952,6 @@ private fun ModuleList(
text = stringResource(R.string.module_empty), text = stringResource(R.string.module_empty),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -665,10 +976,8 @@ private fun ModuleList(
}, },
onCheckChanged = { onCheckChanged = {
scope.launch { scope.launch {
val success = loadingDialog.withLoading { val success = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) { toggleModule(module.dirId, !module.enabled)
toggleModule(module.dirId, !module.enabled)
}
} }
if (success) { if (success) {
viewModel.fetchModuleList() viewModel.fetchModuleList()
@@ -724,23 +1033,30 @@ fun ModuleItem(
onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, onUpdate: (ModuleViewModel.ModuleInfo) -> Unit,
onClick: (ModuleViewModel.ModuleInfo) -> Unit onClick: (ModuleViewModel.ModuleInfo) -> Unit
) { ) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
val isHideTagRow = prefs.getBoolean("is_hide_tag_row", false)
// 获取显示更多模块信息的设置
val showMoreModuleInfo = prefs.getBoolean("show_more_module_info", false)
// 剪贴板管理器和触觉反馈
val clipboardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
val hapticFeedback = LocalHapticFeedback.current
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
val textDecoration = if (!module.remove) null else TextDecoration.LineThrough val textDecoration = if (!module.remove) null else TextDecoration.LineThrough
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val indication = LocalIndication.current val indication = LocalIndication.current
val viewModel = viewModel<ModuleViewModel>() val viewModel = viewModel<ModuleViewModel>()
// 使用缓存系统获取模块大小
val sizeStr = remember(module.dirId) {
viewModel.getModuleSize(module.dirId)
}
Column( Column(
modifier = Modifier modifier = Modifier
.run { .run {
@@ -770,15 +1086,48 @@ fun ModuleItem(
Column( Column(
modifier = Modifier.fillMaxWidth(0.8f) modifier = Modifier.fillMaxWidth(0.8f)
) { ) {
Text( Row(
text = module.name, verticalAlignment = Alignment.CenterVertically,
fontSize = MaterialTheme.typography.titleMedium.fontSize, horizontalArrangement = Arrangement.spacedBy(8.dp)
fontWeight = FontWeight.SemiBold, ) {
lineHeight = MaterialTheme.typography.bodySmall.lineHeight, Text(
fontFamily = MaterialTheme.typography.titleMedium.fontFamily, text = module.name,
textDecoration = textDecoration, fontSize = MaterialTheme.typography.titleMedium.fontSize,
color = MaterialTheme.colorScheme.onSurface fontWeight = FontWeight.SemiBold,
) lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.titleMedium.fontFamily,
textDecoration = textDecoration,
modifier = Modifier.weight(1f, false)
)
// 显示验证标签
if (module.isVerified) {
Surface(
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
) {
Icon(
imageVector = Icons.Default.Verified,
contentDescription = stringResource(R.string.module_signature_verified),
tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(12.dp)
)
Spacer(modifier = Modifier.width(2.dp))
Text(
text = stringResource(R.string.module_verified),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimary,
fontWeight = FontWeight.Medium
)
}
}
}
}
Text( Text(
text = "$moduleVersion: ${module.version}", text = "$moduleVersion: ${module.version}",
@@ -786,7 +1135,6 @@ fun ModuleItem(
lineHeight = MaterialTheme.typography.bodySmall.lineHeight, lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily, fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration, textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
@@ -795,8 +1143,44 @@ fun ModuleItem(
lineHeight = MaterialTheme.typography.bodySmall.lineHeight, lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily, fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration, textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
// 显示更多模块信息时添加updateJson
if (showMoreModuleInfo && module.updateJson.isNotEmpty()) {
val updateJsonLabel = stringResource(R.string.module_update_json)
Text(
text = "$updateJsonLabel: ${module.updateJson}",
fontSize = MaterialTheme.typography.bodySmall.fontSize,
lineHeight = MaterialTheme.typography.bodySmall.lineHeight,
fontFamily = MaterialTheme.typography.bodySmall.fontFamily,
textDecoration = textDecoration,
color = MaterialTheme.colorScheme.primary,
maxLines = 5,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
},
onLongClick = {
// 长按复制updateJson地址
val clipData = ClipData.newPlainText(
"Update JSON URL",
module.updateJson
)
clipboardManager.setPrimaryClip(clipData)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
// 显示复制成功的提示
Toast.makeText(
context,
context.getString(R.string.module_update_json_copied),
Toast.LENGTH_SHORT
).show()
}
),
)
}
} }
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
@@ -810,14 +1194,6 @@ fun ModuleItem(
checked = module.enabled, checked = module.enabled,
onCheckedChange = onCheckChanged, onCheckedChange = onCheckChanged,
interactionSource = if (!module.hasWebUi) interactionSource else null, interactionSource = if (!module.hasWebUi) interactionSource else null,
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
) )
} }
} }
@@ -833,8 +1209,45 @@ fun ModuleItem(
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
maxLines = 4, maxLines = 4,
textDecoration = textDecoration, textDecoration = textDecoration,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
if (!isHideTagRow) {
Spacer(modifier = Modifier.height(12.dp))
// 文件夹名称和大小标签
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
) {
Text(
text = module.dirId,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
color = MaterialTheme.colorScheme.onPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier
) {
Text(
text = sizeStr,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
color = MaterialTheme.colorScheme.onSecondaryContainer,
maxLines = 1
)
}
}
}
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -855,7 +1268,6 @@ fun ModuleItem(
viewModel.markNeedRefresh() viewModel.markNeedRefresh()
}, },
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = ButtonDefaults.filledTonalButtonColors()
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
@@ -872,9 +1284,8 @@ fun ModuleItem(
onClick = { onClick(module) }, onClick = { onClick(module) },
interactionSource = interactionSource, interactionSource = interactionSource,
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = ButtonDefaults.filledTonalButtonColors()
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, imageVector = Icons.AutoMirrored.Outlined.Wysiwyg,
@@ -892,7 +1303,6 @@ fun ModuleItem(
onClick = { onUpdate(module) }, onClick = { onUpdate(module) },
shape = ButtonDefaults.textShape, shape = ButtonDefaults.textShape,
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = ButtonDefaults.filledTonalButtonColors()
) { ) {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
@@ -906,8 +1316,6 @@ fun ModuleItem(
modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp),
onClick = { onUninstallClicked(module) }, onClick = { onUninstallClicked(module) },
contentPadding = ButtonDefaults.TextButtonContentPadding, contentPadding = ButtonDefaults.TextButtonContentPadding,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer)
) { ) {
if (!module.remove) { if (!module.remove) {
Icon( Icon(
@@ -946,6 +1354,8 @@ fun ModuleItemPreview() {
hasActionScript = false, hasActionScript = false,
dirId = "dirId", dirId = "dirId",
config = ModuleConfig(), config = ModuleConfig(),
isVerified = true,
verificationTimestamp = System.currentTimeMillis()
) )
ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {}) ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {})
} }

File diff suppressed because it is too large Load Diff

View File

@@ -207,13 +207,13 @@ private fun TemplateItem(
) )
Text(template.description) Text(template.description)
FlowRow { FlowRow {
LabelText(label = "UID: ${template.uid}", backgroundColor = MaterialTheme.colorScheme.surface) LabelText(label = "UID: ${template.uid}")
LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface) LabelText(label = "GID: ${template.gid}")
LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface) LabelText(label = template.context,)
if (template.local) { if (template.local) {
LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface) LabelText(label = "local")
} else { } else {
LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface) LabelText(label = "remote")
} }
} }
} }
@@ -231,7 +231,12 @@ private fun TopBar(
colors: TopAppBarColors, colors: TopAppBarColors,
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.surfaceContainerLow val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = CardConfig.cardAlpha val cardAlpha = CardConfig.cardAlpha
TopAppBar( TopAppBar(

View File

@@ -10,19 +10,15 @@ 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 settingElevation: Dp = 4.dp
val customBackgroundElevation: Dp = 0.dp
// 卡片透明度 // 卡片透明度
var cardAlpha by mutableFloatStateOf(1f) var cardAlpha by mutableFloatStateOf(1f)
// 卡片亮度 // 卡片亮度
var cardDim by mutableFloatStateOf(0f) var cardDim by mutableFloatStateOf(0f)
// 卡片阴影 // 卡片阴影
var cardElevation by mutableStateOf(settingElevation) 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 isCustomDimSet by mutableStateOf(false)
@@ -69,37 +65,18 @@ object CardConfig {
*/ */
fun updateShadowEnabled(enabled: Boolean) { fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled isShadowEnabled = enabled
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) { cardElevation = 0.dp
customBackgroundElevation
} else if (enabled) {
settingElevation
} else {
customBackgroundElevation
}
} }
/** /**
* 设置深色模式默认值 * 设置主题模式默认值
*/ */
fun setDarkModeDefaults() { fun setThemeDefaults(isDarkMode: Boolean) {
if (!isCustomAlphaSet) { if (!isCustomAlphaSet) {
cardAlpha = 1f cardAlpha = 1f
} }
if (!isCustomDimSet) { if (!isCustomDimSet) {
cardDim = 0.5f cardDim = if (isDarkMode) 0.5f else 0f
}
updateShadowEnabled(isShadowEnabled)
}
/**
* 设置浅色模式默认值
*/
fun setLightModeDefaults() {
if (!isCustomAlphaSet) {
cardAlpha = 1f
}
if (!isCustomDimSet) {
cardDim = 0f
} }
updateShadowEnabled(isShadowEnabled) updateShadowEnabled(isShadowEnabled)
} }
@@ -114,6 +91,19 @@ fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
contentColor = determineContentColor(originalColor) contentColor = determineContentColor(originalColor)
) )
/**
* 获取卡片阴影配置
*/
@Composable
fun getCardElevation() = CardDefaults.cardElevation(
defaultElevation = CardConfig.cardElevation,
pressedElevation = CardConfig.cardElevation,
focusedElevation = CardConfig.cardElevation,
hoveredElevation = CardConfig.cardElevation,
draggedElevation = CardConfig.cardElevation,
disabledElevation = CardConfig.cardElevation
)
/** /**
* 根据背景颜色、主题模式和用户设置确定内容颜色 * 根据背景颜色、主题模式和用户设置确定内容颜色
*/ */

View File

@@ -47,6 +47,7 @@ import com.sukisu.ultra.ui.util.saveTransformedBackground
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
@@ -150,9 +151,9 @@ fun KernelSUTheme(
// 根据暗色模式和自定义背景调整卡片配置 // 根据暗色模式和自定义背景调整卡片配置
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
if (darkTheme && !dynamicColor) { if (darkTheme && !dynamicColor) {
CardConfig.setDarkModeDefaults() CardConfig.setThemeDefaults(true)
} else if (!darkTheme && !dynamicColor) { } else if (!darkTheme && !dynamicColor) {
CardConfig.setLightModeDefaults() CardConfig.setThemeDefaults(false)
} }
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground) CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
@@ -216,7 +217,8 @@ fun KernelSUTheme(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.zIndex(-2f) .zIndex(-2f)
.background(if (darkTheme) MaterialTheme.colorScheme.surfaceContainerLow else MaterialTheme.colorScheme.surfaceContainerLow) .background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
) )
// 自定义背景层 // 自定义背景层
@@ -287,24 +289,32 @@ fun KernelSUTheme(
*/ */
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
private fun createDynamicDarkColorScheme(context: Context) = private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
dynamicDarkColorScheme(context).copy( val scheme = dynamicDarkColorScheme(context)
background = Color.Transparent, return scheme.copy(
surface = Color.Transparent, background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
onBackground = Color.White, surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
onSurface = Color.White onBackground = scheme.onBackground,
onSurface = scheme.onSurface
) )
}
/** /**
* 创建动态浅色颜色方案 * 创建动态浅色颜色方案
*/ */
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
private fun createDynamicLightColorScheme(context: Context) = private fun createDynamicLightColorScheme(context: Context): ColorScheme {
dynamicLightColorScheme(context).copy( val scheme = dynamicLightColorScheme(context)
background = Color.Transparent, return scheme.copy(
surface = Color.Transparent background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
onBackground = scheme.onBackground,
onSurface = scheme.onSurface
) )
}
/** /**
* 创建深色颜色方案 * 创建深色颜色方案
@@ -327,9 +337,9 @@ private fun createDarkColorScheme() = darkColorScheme(
onError = ThemeConfig.currentTheme.onErrorDark, onError = ThemeConfig.currentTheme.onErrorDark,
errorContainer = ThemeConfig.currentTheme.errorContainerDark, errorContainer = ThemeConfig.currentTheme.errorContainerDark,
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark, onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark,
background = Color.Transparent, background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundDark,
onBackground = ThemeConfig.currentTheme.onBackgroundDark, onBackground = ThemeConfig.currentTheme.onBackgroundDark,
surface = Color.Transparent, surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceDark,
onSurface = ThemeConfig.currentTheme.onSurfaceDark, onSurface = ThemeConfig.currentTheme.onSurfaceDark,
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark, surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark,
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark, onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark,
@@ -369,9 +379,9 @@ private fun createLightColorScheme() = lightColorScheme(
onError = ThemeConfig.currentTheme.onErrorLight, onError = ThemeConfig.currentTheme.onErrorLight,
errorContainer = ThemeConfig.currentTheme.errorContainerLight, errorContainer = ThemeConfig.currentTheme.errorContainerLight,
onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight, onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight,
background = Color.Transparent, background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundLight,
onBackground = ThemeConfig.currentTheme.onBackgroundLight, onBackground = ThemeConfig.currentTheme.onBackgroundLight,
surface = Color.Transparent, surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceLight,
onSurface = ThemeConfig.currentTheme.onSurfaceLight, onSurface = ThemeConfig.currentTheme.onSurfaceLight,
surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight, surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight,
onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight, onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight,
@@ -390,6 +400,7 @@ private fun createLightColorScheme() = lightColorScheme(
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight, surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight,
) )
/** /**
* 复制图片到应用内部存储并提升持久性 * 复制图片到应用内部存储并提升持久性
*/ */

View File

@@ -223,7 +223,11 @@ fun restoreBoot(
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean { ): Boolean {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val result = flashWithIO("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, onStderr) val result = flashWithIO(
"${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot",
onStdout,
onStderr
)
onFinish(result.isSuccess, result.code) onFinish(result.isSuccess, result.code)
return result.isSuccess return result.isSuccess
} }
@@ -232,7 +236,8 @@ fun uninstallPermanently(
onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit
): Boolean { ): Boolean {
val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so")
val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) val result =
flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr)
onFinish(result.isSuccess, result.code) onFinish(result.isSuccess, result.code)
return result.isSuccess return result.isSuccess
} }
@@ -457,6 +462,7 @@ fun getSuSFSVariant(): String {
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant") val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant")
return result return result
} }
fun getSuSFSFeatures(): String { fun getSuSFSFeatures(): String {
val shell = getRootShell() val shell = getRootShell()
val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features") val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features")
@@ -505,7 +511,7 @@ fun getKpmModuleCount(): Int {
return result.trim().toIntOrNull() ?: 0 return result.trim().toIntOrNull() ?: 0
} }
fun runCmd(shell : Shell, cmd : String) : String { fun runCmd(shell: Shell, cmd: String): String {
return shell.newJob() return shell.newJob()
.add(cmd) .add(cmd)
.to(mutableListOf<String>(), null) .to(mutableListOf<String>(), null)
@@ -548,3 +554,18 @@ fun getKpmVersion(): String {
val result = ShellUtils.fastCmd(shell, cmd) val result = ShellUtils.fastCmd(shell, cmd)
return result.trim() return result.trim()
} }
fun getZygiskImplement(): String {
val shell = getRootShell()
val zygiskPath = "/data/adb/modules/zygisksu"
val rezygiskPath = "/data/adb/modules/rezygisk"
val result = if (ShellUtils.fastCmdResult(shell, "test -f $zygiskPath/module.prop && test ! -f $zygiskPath/disable")) {
ShellUtils.fastCmd(shell, "grep '^name=' $zygiskPath/module.prop | cut -d'=' -f2")
} else if (ShellUtils.fastCmdResult(shell, "test -f $rezygiskPath/module.prop && test ! -f $rezygiskPath/disable")) {
ShellUtils.fastCmd(shell, "grep '^name=' $rezygiskPath/module.prop | cut -d'=' -f2")
} else {
"None"
}
Log.i(TAG, "Zygisk implement: $result")
return result
}

View File

@@ -1,17 +1,14 @@
package com.sukisu.ultra.ui.util package com.sukisu.ultra.ui.util
import android.app.AlertDialog
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
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.material3.SnackbarDuration import androidx.compose.material3.*
import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.*
import androidx.compose.material3.SnackbarResult import androidx.compose.ui.platform.LocalContext
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -25,18 +22,78 @@ import java.util.Date
import java.util.Locale import java.util.Locale
object ModuleModify { object ModuleModify {
suspend fun showRestoreConfirmation(context: Context): Boolean { @Composable
val result = CompletableDeferred<Boolean>() fun RestoreConfirmationDialog(
withContext(Dispatchers.Main) { showDialog: Boolean,
AlertDialog.Builder(context) onConfirm: () -> Unit,
.setTitle(context.getString(R.string.restore_confirm_title)) onDismiss: () -> Unit
.setMessage(context.getString(R.string.restore_confirm_message)) ) {
.setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) } val context = LocalContext.current
.setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) }
.setOnCancelListener { result.complete(false) } if (showDialog) {
.show() AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = context.getString(R.string.restore_confirm_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = context.getString(R.string.restore_confirm_message),
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(context.getString(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(context.getString(R.string.cancel))
}
}
)
}
}
@Composable
fun AllowlistRestoreConfirmationDialog(
showDialog: Boolean,
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
val context = LocalContext.current
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = context.getString(R.string.allowlist_restore_confirm_title),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = context.getString(R.string.allowlist_restore_confirm_message),
style = MaterialTheme.typography.bodyMedium
)
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(context.getString(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(context.getString(R.string.cancel))
}
}
)
} }
return result.await()
} }
suspend fun backupModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { suspend fun backupModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
@@ -82,8 +139,19 @@ object ModuleModify {
} }
} }
suspend fun restoreModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { suspend fun restoreModules(
val userConfirmed = showRestoreConfirmation(context) context: Context,
snackBarHost: SnackbarHostState,
uri: Uri,
showConfirmDialog: (Boolean) -> Unit,
confirmResult: CompletableDeferred<Boolean>
) {
// 显示确认对话框
withContext(Dispatchers.Main) {
showConfirmDialog(true)
}
val userConfirmed = confirmResult.await()
if (!userConfirmed) return if (!userConfirmed) return
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -132,20 +200,6 @@ object ModuleModify {
} }
} }
suspend fun showAllowlistRestoreConfirmation(context: Context): Boolean {
val result = CompletableDeferred<Boolean>()
withContext(Dispatchers.Main) {
AlertDialog.Builder(context)
.setTitle(context.getString(R.string.allowlist_restore_confirm_title))
.setMessage(context.getString(R.string.allowlist_restore_confirm_message))
.setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) }
.setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) }
.setOnCancelListener { result.complete(false) }
.show()
}
return result.await()
}
suspend fun backupAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { suspend fun backupAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
@@ -182,8 +236,19 @@ object ModuleModify {
} }
} }
suspend fun restoreAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { suspend fun restoreAllowlist(
val userConfirmed = showAllowlistRestoreConfirmation(context) context: Context,
snackBarHost: SnackbarHostState,
uri: Uri,
showConfirmDialog: (Boolean) -> Unit,
confirmResult: CompletableDeferred<Boolean>
) {
// 显示确认对话框
withContext(Dispatchers.Main) {
showConfirmDialog(true)
}
val userConfirmed = confirmResult.await()
if (!userConfirmed) return if (!userConfirmed) return
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -246,13 +311,42 @@ object ModuleModify {
context: Context, context: Context,
snackBarHost: SnackbarHostState, snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult( ): androidx.activity.result.ActivityResultLauncher<Intent> {
contract = ActivityResultContracts.StartActivityForResult() var showRestoreDialog by remember { mutableStateOf(false) }
) { result -> var restoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
if (result.resultCode == android.app.Activity.RESULT_OK) { var pendingUri by remember { mutableStateOf<Uri?>(null) }
result.data?.data?.let { uri ->
scope.launch { // 显示恢复确认对话框
restoreModules(context, snackBarHost, uri) RestoreConfirmationDialog(
showDialog = showRestoreDialog,
onConfirm = {
showRestoreDialog = false
restoreConfirmResult?.complete(true)
},
onDismiss = {
showRestoreDialog = false
restoreConfirmResult?.complete(false)
}
)
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
pendingUri = uri
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()
restoreConfirmResult = confirmResult
restoreModules(
context = context,
snackBarHost = snackBarHost,
uri = uri,
showConfirmDialog = { show -> showRestoreDialog = show },
confirmResult = confirmResult
)
}
} }
} }
} }
@@ -280,13 +374,42 @@ object ModuleModify {
context: Context, context: Context,
snackBarHost: SnackbarHostState, snackBarHost: SnackbarHostState,
scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope()
) = rememberLauncherForActivityResult( ): androidx.activity.result.ActivityResultLauncher<Intent> {
contract = ActivityResultContracts.StartActivityForResult() var showAllowlistRestoreDialog by remember { mutableStateOf(false) }
) { result -> var allowlistRestoreConfirmResult by remember { mutableStateOf<CompletableDeferred<Boolean>?>(null) }
if (result.resultCode == android.app.Activity.RESULT_OK) { var pendingUri by remember { mutableStateOf<Uri?>(null) }
result.data?.data?.let { uri ->
scope.launch { // 显示允许列表恢复确认对话框
restoreAllowlist(context, snackBarHost, uri) AllowlistRestoreConfirmationDialog(
showDialog = showAllowlistRestoreDialog,
onConfirm = {
showAllowlistRestoreDialog = false
allowlistRestoreConfirmResult?.complete(true)
},
onDismiss = {
showAllowlistRestoreDialog = false
allowlistRestoreConfirmResult?.complete(false)
}
)
return rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
pendingUri = uri
scope.launch {
val confirmResult = CompletableDeferred<Boolean>()
allowlistRestoreConfirmResult = confirmResult
restoreAllowlist(
context = context,
snackBarHost = snackBarHost,
uri = uri,
showConfirmDialog = { show -> showAllowlistRestoreDialog = show },
confirmResult = confirmResult
)
}
} }
} }
} }

View File

@@ -33,7 +33,7 @@ object ModuleUtils {
} }
}?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module) }?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module)
var formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim() val formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
var moduleName = formattedFileName var moduleName = formattedFileName
try { try {
@@ -52,12 +52,10 @@ object ModuleUtils {
if (entry.name == "module.prop") { if (entry.name == "module.prop") {
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8)) val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
var line: String? var line: String?
var nameFound = false
while (reader.readLine().also { line = it } != null) { while (reader.readLine().also { line = it } != null) {
if (line?.startsWith("name=") == true) { if (line?.startsWith("name=") == true) {
moduleName = line.substringAfter("=") moduleName = line.substringAfter("=")
moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim() moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
nameFound = true
break break
} }
} }
@@ -102,4 +100,43 @@ object ModuleUtils {
Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}") Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}")
} }
} }
fun extractModuleId(context: Context, uri: Uri): String? {
if (uri == Uri.EMPTY) {
return null
}
return try {
val inputStream = context.contentResolver.openInputStream(uri)
if (inputStream == null) {
return null
}
val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry
var moduleId: String? = null
// 遍历ZIP文件中的条目查找module.prop文件
while (entry != null) {
if (entry.name == "module.prop") {
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
var line: String?
while (reader.readLine().also { line = it } != null) {
if (line?.startsWith("id=") == true) {
moduleId = line.substringAfter("=").trim()
break
}
}
break
}
entry = zipInputStream.nextEntry
}
zipInputStream.close()
moduleId
} catch (e: Exception) {
Log.e(TAG, "提取模块ID时发生异常: ${e.message}", e)
null
}
}
} }

View File

@@ -0,0 +1,232 @@
package com.sukisu.ultra.ui.util
import android.content.Context
import android.net.Uri
import android.util.Log
import com.sukisu.ultra.Natives
import java.io.File
import java.io.FileOutputStream
/**
* @author ShirkNeko
* @date 2025/8/3
*/
// 模块签名验证工具类
object ModuleSignatureUtils {
private const val TAG = "ModuleSignatureUtils"
fun verifyModuleSignature(context: Context, moduleUri: Uri): Boolean {
return try {
// 创建临时文件
val tempFile = File(context.cacheDir, "temp_module_${System.currentTimeMillis()}.zip")
// 复制URI内容到临时文件
context.contentResolver.openInputStream(moduleUri)?.use { inputStream ->
FileOutputStream(tempFile).use { outputStream ->
inputStream.copyTo(outputStream)
}
}
// 调用native方法验证签名
val isVerified = Natives.verifyModuleSignature(tempFile.absolutePath)
// 清理临时文件
tempFile.delete()
Log.d(TAG, "Module signature verification result: $isVerified")
isVerified
} catch (e: Exception) {
Log.e(TAG, "Error verifying module signature", e)
false
}
}
}
// 验证模块签名
fun verifyModuleSignature(context: Context, moduleUri: Uri): Boolean {
return ModuleSignatureUtils.verifyModuleSignature(context, moduleUri)
}
object ModuleOperationUtils {
private const val TAG = "ModuleOperationUtils"
fun handleModuleInstallSuccess(context: Context, moduleUri: Uri, isSignatureVerified: Boolean) {
if (!isSignatureVerified) {
Log.d(TAG, "模块签名未验证,跳过创建验证标志")
return
}
try {
// 从ZIP文件提取模块ID
val moduleId = ModuleUtils.extractModuleId(context, moduleUri)
if (moduleId == null) {
Log.e(TAG, "无法提取模块ID无法创建验证标志")
return
}
// 创建验证标志文件
val success = ModuleVerificationManager.createVerificationFlag(moduleId)
if (success) {
Log.d(TAG, "模块 $moduleId 验证标志创建成功")
} else {
Log.e(TAG, "模块 $moduleId 验证标志创建失败")
}
} catch (e: Exception) {
Log.e(TAG, "处理模块安装成功时发生异常", e)
}
}
fun handleModuleUninstall(moduleId: String) {
try {
val success = ModuleVerificationManager.removeVerificationFlag(moduleId)
if (success) {
Log.d(TAG, "模块 $moduleId 验证标志移除成功")
} else {
Log.d(TAG, "模块 $moduleId 验证标志移除失败或不存在")
}
} catch (e: Exception) {
Log.e(TAG, "处理模块卸载时发生异常: $moduleId", e)
}
}
fun handleModuleUpdate(context: Context, moduleUri: Uri, isSignatureVerified: Boolean) {
try {
val moduleId = ModuleUtils.extractModuleId(context, moduleUri)
if (moduleId == null) {
Log.e(TAG, "无法提取模块ID无法处理验证标志")
return
}
if (isSignatureVerified) {
// 签名验证通过,创建或更新验证标志
val success = ModuleVerificationManager.createVerificationFlag(moduleId)
if (success) {
Log.d(TAG, "模块 $moduleId 更新后验证标志已更新")
} else {
Log.e(TAG, "模块 $moduleId 更新后验证标志更新失败")
}
} else {
// 签名验证失败,移除验证标志
ModuleVerificationManager.removeVerificationFlag(moduleId)
Log.d(TAG, "模块 $moduleId 更新后签名未验证,验证标志已移除")
}
} catch (e: Exception) {
Log.e(TAG, "处理模块更新时发生异常", e)
}
}
}
object ModuleVerificationManager {
private const val TAG = "ModuleVerificationManager"
private const val VERIFICATION_FLAGS_DIR = "/data/adb/ksu/verified_modules"
// 为指定模块创建验证标志文件
fun createVerificationFlag(moduleId: String): Boolean {
return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
// 确保目录存在
val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'"
shell.newJob().add(createDirCommand).exec()
// 创建验证标志文件,写入验证时间戳
val timestamp = System.currentTimeMillis()
val command = "echo '$timestamp' > '$flagFilePath'"
val result = shell.newJob().add(command).exec()
if (result.isSuccess) {
Log.d(TAG, "验证标志文件创建成功: $flagFilePath")
true
} else {
Log.e(TAG, "验证标志文件创建失败: $moduleId")
false
}
} catch (e: Exception) {
Log.e(TAG, "创建验证标志文件时发生异常: $moduleId", e)
false
}
}
fun removeVerificationFlag(moduleId: String): Boolean {
return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
val command = "rm -f '$flagFilePath'"
val result = shell.newJob().add(command).exec()
if (result.isSuccess) {
Log.d(TAG, "验证标志文件移除成功: $flagFilePath")
true
} else {
Log.e(TAG, "验证标志文件移除失败: $moduleId")
false
}
} catch (e: Exception) {
Log.e(TAG, "移除验证标志文件时发生异常: $moduleId", e)
false
}
}
fun getVerificationTimestamp(moduleId: String): Long {
return try {
val shell = getRootShell()
val flagFilePath = "$VERIFICATION_FLAGS_DIR/$moduleId"
val command = "cat '$flagFilePath' 2>/dev/null || echo '0'"
val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess && result.out.isNotEmpty()) {
val timestampStr = result.out.firstOrNull()?.trim() ?: "0"
timestampStr.toLongOrNull() ?: 0L
} else {
0L
}
} catch (e: Exception) {
Log.e(TAG, "获取验证时间戳时发生异常: $moduleId", e)
0L
}
}
fun batchCheckVerificationStatus(moduleIds: List<String>): Map<String, Boolean> {
if (moduleIds.isEmpty()) return emptyMap()
return try {
val shell = getRootShell()
val result = mutableMapOf<String, Boolean>()
// 确保目录存在
val createDirCommand = "mkdir -p '$VERIFICATION_FLAGS_DIR'"
shell.newJob().add(createDirCommand).exec()
// 批量检查所有模块的验证标志文件
val commands = moduleIds.map { moduleId ->
"test -f '$VERIFICATION_FLAGS_DIR/$moduleId' && echo '$moduleId:true' || echo '$moduleId:false'"
}
val command = commands.joinToString(" && ")
val shellResult = shell.newJob().add(command).to(ArrayList(), null).exec()
if (shellResult.isSuccess) {
shellResult.out.forEach { line ->
val parts = line.split(":")
if (parts.size == 2) {
val moduleId = parts[0]
val isVerified = parts[1] == "true"
result[moduleId] = isVerified
}
}
}
Log.d(TAG, "批量验证检查完成,共检查 ${moduleIds.size} 个模块")
result
} catch (e: Exception) {
Log.e(TAG, "批量检查验证状态时发生异常", e)
// 返回默认值,所有模块都标记为未验证
moduleIds.associateWith { false }
}
}
}

View File

@@ -0,0 +1,44 @@
package com.sukisu.ultra.ui.util
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import com.sukisu.ultra.ui.MainActivity
/**
* 重启应用程序
**/
fun Context.restartApp(
activityClass: Class<out Activity>,
finishCurrent: Boolean = true,
clearTask: Boolean = true,
newTask: Boolean = true
) {
val intent = Intent(this, activityClass)
if (clearTask) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
if (finishCurrent && this is Activity) {
finish()
}
}
/**
* 刷新启动器图标
*/
fun toggleLauncherIcon(context: Context, useAlt: Boolean) {
val pm = context.packageManager
val main = ComponentName(context, MainActivity::class.java.name)
val alt = ComponentName(context, "${MainActivity::class.java.name}Alias")
if (useAlt) {
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
} else {
pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP)
pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,540 @@
package com.sukisu.ultra.ui.util
import android.annotation.SuppressLint
/**
* Magisk模块脚本生成器
* 用于生成各种启动脚本的内容
*/
object ScriptGenerator {
// 常量定义
private const val DEFAULT_UNAME = "default"
private const val DEFAULT_BUILD_TIME = "default"
private const val LOG_DIR = "/data/adb/ksu/log"
/**
* 生成所有脚本文件
*/
fun generateAllScripts(config: SuSFSManager.ModuleConfig): Map<String, String> {
return mapOf(
"service.sh" to generateServiceScript(config),
"post-fs-data.sh" to generatePostFsDataScript(config),
"post-mount.sh" to generatePostMountScript(config),
"boot-completed.sh" to generateBootCompletedScript(config)
)
}
// 日志相关的通用脚本片段
private fun generateLogSetup(logFileName: String): String = """
# 日志目录
LOG_DIR="$LOG_DIR"
LOG_FILE="${'$'}LOG_DIR/$logFileName"
# 创建日志目录
mkdir -p "${'$'}LOG_DIR"
# 获取当前时间
get_current_time() {
date '+%Y-%m-%d %H:%M:%S'
}
""".trimIndent()
// 二进制文件检查的通用脚本片段
private fun generateBinaryCheck(targetPath: String): String = """
# 检查SuSFS二进制文件
SUSFS_BIN="$targetPath"
if [ ! -f "${'$'}SUSFS_BIN" ]; then
echo "$(get_current_time): SuSFS二进制文件未找到: ${'$'}SUSFS_BIN" >> "${'$'}LOG_FILE"
exit 1
fi
""".trimIndent()
/**
* 生成service.sh脚本内容
*/
@SuppressLint("SdCardPath")
private fun generateServiceScript(config: SuSFSManager.ModuleConfig): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Service Script")
appendLine("# 在系统服务启动后执行")
appendLine()
appendLine(generateLogSetup("susfs_service.log"))
appendLine()
appendLine(generateBinaryCheck(config.targetPath))
appendLine()
if (shouldConfigureInService(config)) {
// 添加SUS路径 (仅在不支持隐藏挂载时)
if (!config.support158 && config.susPaths.isNotEmpty()) {
appendLine()
appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done")
appendLine("sleep 45")
generateSusPathsSection(config.susPaths)
}
// 设置uname和构建时间
generateUnameSection(config)
// 添加Kstat配置
generateKstatSection(config.kstatConfigs, config.addKstatPaths)
}
// 添加日志设置
generateLogSettingSection(config.enableLog)
// 隐藏BL相关配置
if (config.enableHideBl) {
generateHideBlSection()
}
// 清理工具残留
if (config.enableCleanupResidue) {
generateCleanupResidueSection()
}
appendLine("echo \"$(get_current_time): Service脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
/**
* 判断是否需要在service中配置
*/
private fun shouldConfigureInService(config: SuSFSManager.ModuleConfig): Boolean {
return config.susPaths.isNotEmpty() ||
config.susLoopPaths.isNotEmpty() ||
config.kstatConfigs.isNotEmpty() ||
config.addKstatPaths.isNotEmpty() ||
(!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME))
}
private fun StringBuilder.generateLogSettingSection(enableLog: Boolean) {
appendLine("# 设置日志启用状态")
val logValue = if (enableLog) 1 else 0
appendLine("\"${'$'}SUSFS_BIN\" enable_log $logValue")
appendLine("echo \"$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
private fun StringBuilder.generateAvcLogSpoofingSection(enableAvcLogSpoofing: Boolean) {
appendLine("# 设置AVC日志欺骗状态")
val avcLogValue = if (enableAvcLogSpoofing) 1 else 0
appendLine("\"${'$'}SUSFS_BIN\" enable_avc_log_spoofing $avcLogValue")
appendLine("echo \"$(get_current_time): AVC日志欺骗功能设置为: ${if (enableAvcLogSpoofing) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
private fun StringBuilder.generateSusPathsSection(susPaths: Set<String>) {
if (susPaths.isNotEmpty()) {
appendLine("# 添加SUS路径")
susPaths.forEach { path ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_path '$path'")
appendLine("echo \"$(get_current_time): 添加SUS路径: $path\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
}
private fun StringBuilder.generateSusLoopPathsSection(susLoopPaths: Set<String>) {
if (susLoopPaths.isNotEmpty()) {
appendLine("# 添加SUS循环路径")
susLoopPaths.forEach { path ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_path_loop '$path'")
appendLine("echo \"$(get_current_time): 添加SUS循环路径: $path\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
}
@SuppressLint("SdCardPath")
private fun StringBuilder.generateKstatSection(
kstatConfigs: Set<String>,
addKstatPaths: Set<String>
) {
// 添加Kstat路径
if (addKstatPaths.isNotEmpty()) {
appendLine("# 添加Kstat路径")
addKstatPaths.forEach { path ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat '$path'")
appendLine("echo \"$(get_current_time): 添加Kstat路径: $path\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
// 添加Kstat静态配置
if (kstatConfigs.isNotEmpty()) {
appendLine("# 添加Kstat静态配置")
kstatConfigs.forEach { config ->
val parts = config.split("|")
if (parts.size >= 13) {
val path = parts[0]
val params = parts.drop(1).joinToString("' '", "'", "'")
appendLine()
appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat_statically '$path' $params")
appendLine("echo \"$(get_current_time): 添加Kstat静态配置: $path\" >> \"${'$'}LOG_FILE\"")
appendLine()
appendLine("\"${'$'}SUSFS_BIN\" update_sus_kstat '$path'")
appendLine("echo \"$(get_current_time): 更新Kstat配置: $path\" >> \"${'$'}LOG_FILE\"")
}
}
appendLine()
}
}
private fun StringBuilder.generateUnameSection(config: SuSFSManager.ModuleConfig) {
if (!config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) {
appendLine("# 设置uname和构建时间")
appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'")
appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
}
private fun StringBuilder.generateHideBlSection() {
appendLine("# 隐藏BL 来自 Shamiko 脚本")
appendLine(
"""
RESETPROP_BIN="/data/adb/ksu/bin/resetprop"
check_reset_prop() {
local NAME=$1
local EXPECTED=$2
local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME)
[ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
}
check_missing_prop() {
local NAME=$1
local EXPECTED=$2
local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME)
[ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
}
check_missing_match_prop() {
local NAME=$1
local EXPECTED=$2
local VALUE=$("${'$'}RESETPROP_BIN" ${'$'}NAME)
[ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
[ -z ${'$'}VALUE ] && "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}EXPECTED
}
contains_reset_prop() {
local NAME=$1
local CONTAINS=$2
local NEWVAL=$3
case "$("${'$'}RESETPROP_BIN" ${'$'}NAME)" in
*"${'$'}CONTAINS"*) "${'$'}RESETPROP_BIN" ${'$'}NAME ${'$'}NEWVAL ;;
esac
}
""".trimIndent())
appendLine()
appendLine("sleep 30")
appendLine()
appendLine("\"${'$'}RESETPROP_BIN\" -w sys.boot_completed 0")
// 添加所有系统属性重置
val systemProps = listOf(
"ro.boot.vbmeta.invalidate_on_error" to "yes",
"ro.boot.vbmeta.avb_version" to "1.2",
"ro.boot.vbmeta.hash_alg" to "sha256",
"ro.boot.vbmeta.size" to "19968",
"ro.boot.vbmeta.device_state" to "locked",
"ro.boot.verifiedbootstate" to "green",
"ro.boot.flash.locked" to "1",
"ro.boot.veritymode" to "enforcing",
"ro.boot.warranty_bit" to "0",
"ro.warranty_bit" to "0",
"ro.debuggable" to "0",
"ro.force.debuggable" to "0",
"ro.secure" to "1",
"ro.adb.secure" to "1",
"ro.build.type" to "user",
"ro.build.tags" to "release-keys",
"ro.vendor.boot.warranty_bit" to "0",
"ro.vendor.warranty_bit" to "0",
"vendor.boot.vbmeta.device_state" to "locked",
"vendor.boot.verifiedbootstate" to "green",
"sys.oem_unlock_allowed" to "0",
"ro.secureboot.lockstate" to "locked",
"ro.boot.realmebootstate" to "green",
"ro.boot.realme.lockstate" to "1",
"ro.crypto.state" to "encrypted"
)
systemProps.forEach { (prop, value) ->
when {
prop.startsWith("ro.boot.vbmeta") && prop.endsWith("_on_error") ->
appendLine("check_missing_prop \"$prop\" \"$value\"")
prop.contains("device_state") || prop.contains("verifiedbootstate") ->
appendLine("check_missing_match_prop \"$prop\" \"$value\"")
else ->
appendLine("check_reset_prop \"$prop\" \"$value\"")
}
}
appendLine()
appendLine("# Hide adb debugging traces")
appendLine("resetprop \"sys.usb.adb.disabled\" \" \"")
appendLine()
appendLine("# Hide recovery boot mode")
appendLine("contains_reset_prop \"ro.bootmode\" \"recovery\" \"unknown\"")
appendLine("contains_reset_prop \"ro.boot.bootmode\" \"recovery\" \"unknown\"")
appendLine("contains_reset_prop \"vendor.boot.bootmode\" \"recovery\" \"unknown\"")
appendLine()
appendLine("# Hide cloudphone detection")
appendLine("[ -n \"$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"")
appendLine()
}
// 清理残留脚本生成
private fun StringBuilder.generateCleanupResidueSection() {
appendLine("# 清理工具残留文件")
appendLine("echo \"$(get_current_time): 开始清理工具残留\" >> \"${'$'}LOG_FILE\"")
appendLine()
// 定义清理函数
appendLine("""
cleanup_path() {
local path="$1"
local desc="$2"
local current="$3"
local total="$4"
if [ -n "${'$'}desc" ]; then
echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path (${'$'}desc)" >> "${'$'}LOG_FILE"
else
echo "$(get_current_time): [${'$'}current/${'$'}total] 清理: ${'$'}path" >> "${'$'}LOG_FILE"
fi
if rm -rf "${'$'}path" 2>/dev/null; then
echo "$(get_current_time): ✓ 成功清理: ${'$'}path" >> "${'$'}LOG_FILE"
else
echo "$(get_current_time): ✗ 清理失败或不存在: ${'$'}path" >> "${'$'}LOG_FILE"
fi
}
""".trimIndent())
appendLine()
appendLine("# 开始清理各种工具残留")
appendLine("TOTAL=33")
appendLine()
val cleanupPaths = listOf(
"/data/local/stryker/" to "Stryker残留",
"/data/system/AppRetention" to "AppRetention残留",
"/data/local/tmp/luckys" to "Luck Tool残留",
"/data/local/tmp/HyperCeiler" to "西米露残留",
"/data/local/tmp/simpleHook" to "simple Hook残留",
"/data/local/tmp/DisabledAllGoogleServices" to "谷歌省电模块残留",
"/data/local/MIO" to "解包软件",
"/data/DNA" to "解包软件",
"/data/local/tmp/cleaner_starter" to "质感清理残留",
"/data/local/tmp/byyang" to "",
"/data/local/tmp/mount_mask" to "",
"/data/local/tmp/mount_mark" to "",
"/data/local/tmp/scriptTMP" to "",
"/data/local/luckys" to "",
"/data/local/tmp/horae_control.log" to "",
"/data/gpu_freq_table.conf" to "",
"/storage/emulated/0/Download/advanced/" to "",
"/storage/emulated/0/Documents/advanced/" to "爱玩机",
"/storage/emulated/0/Android/naki/" to "旧版asoulopt",
"/data/swap_config.conf" to "scene附加模块2",
"/data/local/tmp/resetprop" to "",
"/dev/cpuset/AppOpt/" to "AppOpt模块",
"/storage/emulated/0/Android/Clash/" to "Clash for Magisk模块",
"/storage/emulated/0/Android/Yume-Yunyun/" to "网易云后台优化模块",
"/data/local/tmp/Surfing_update" to "Surfing模块缓存",
"/data/encore/custom_default_cpu_gov" to "encore模块",
"/data/encore/default_cpu_gov" to "encore模块",
"/data/local/tmp/yshell" to "",
"/data/local/tmp/encore_logo.png" to "",
"/storage/emulated/legacy/" to "",
"/storage/emulated/elgg/" to "",
"/data/system/junge/" to "",
"/data/local/tmp/mount_namespace" to "挂载命名空间残留"
)
cleanupPaths.forEachIndexed { index, (path, desc) ->
val current = index + 1
appendLine("cleanup_path '$path' '$desc' $current \$TOTAL")
}
appendLine()
appendLine("echo \"$(get_current_time): 工具残留清理完成\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
/**
* 生成post-fs-data.sh脚本内容
*/
private fun generatePostFsDataScript(config: SuSFSManager.ModuleConfig): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Post-FS-Data Script")
appendLine("# 在文件系统挂载后但在系统完全启动前执行")
appendLine()
appendLine(generateLogSetup("susfs_post_fs_data.log"))
appendLine()
appendLine(generateBinaryCheck(config.targetPath))
appendLine()
appendLine("echo \"$(get_current_time): Post-FS-Data脚本开始执行\" >> \"${'$'}LOG_FILE\"")
appendLine()
// 设置uname和构建时间 - 只有在选择在post-fs-data中执行时才执行
if (config.executeInPostFsData && (config.unameValue != DEFAULT_UNAME || config.buildTimeValue != DEFAULT_BUILD_TIME)) {
appendLine("# 设置uname和构建时间")
appendLine("\"${'$'}SUSFS_BIN\" set_uname '${config.unameValue}' '${config.buildTimeValue}'")
appendLine("echo \"$(get_current_time): 设置uname为: ${config.unameValue}, 构建时间为: ${config.buildTimeValue}\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
generateUmountZygoteIsoServiceSection(config.umountForZygoteIsoService, config.support158)
// 添加AVC日志欺骗设置
generateAvcLogSpoofingSection(config.enableAvcLogSpoofing)
appendLine("echo \"$(get_current_time): Post-FS-Data脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
// 添加新的生成方法
private fun StringBuilder.generateUmountZygoteIsoServiceSection(umountForZygoteIsoService: Boolean, support158: Boolean) {
if (support158) {
appendLine("# 设置Zygote隔离服务卸载状态")
val umountValue = if (umountForZygoteIsoService) 1 else 0
appendLine("\"${'$'}SUSFS_BIN\" umount_for_zygote_iso_service $umountValue")
appendLine("echo \"$(get_current_time): Zygote隔离服务卸载设置为: ${if (umountForZygoteIsoService) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
}
/**
* 生成post-mount.sh脚本内容
*/
private fun generatePostMountScript(config: SuSFSManager.ModuleConfig): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Post-Mount Script")
appendLine("# 在所有分区挂载完成后执行")
appendLine()
appendLine(generateLogSetup("susfs_post_mount.log"))
appendLine()
appendLine("echo \"$(get_current_time): Post-Mount脚本开始执行\" >> \"${'$'}LOG_FILE\"")
appendLine()
appendLine(generateBinaryCheck(config.targetPath))
appendLine()
// 添加SUS挂载
if (config.susMounts.isNotEmpty()) {
appendLine("# 添加SUS挂载")
config.susMounts.forEach { mount ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_mount '$mount'")
appendLine("echo \"$(get_current_time): 添加SUS挂载: $mount\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
// 添加尝试卸载
if (config.tryUmounts.isNotEmpty()) {
appendLine("# 添加尝试卸载")
config.tryUmounts.forEach { umount ->
val parts = umount.split("|")
if (parts.size == 2) {
val path = parts[0]
val mode = parts[1]
appendLine("\"${'$'}SUSFS_BIN\" add_try_umount '$path' $mode")
appendLine("echo \"$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"${'$'}LOG_FILE\"")
}
}
appendLine()
}
appendLine("echo \"$(get_current_time): Post-Mount脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
/**
* 生成boot-completed.sh脚本内容
*/
@SuppressLint("SdCardPath")
private fun generateBootCompletedScript(config: SuSFSManager.ModuleConfig): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Boot-Completed Script")
appendLine("# 在系统完全启动后执行")
appendLine()
appendLine(generateLogSetup("susfs_boot_completed.log"))
appendLine()
appendLine("echo \"$(get_current_time): Boot-Completed脚本开始执行\" >> \"${'$'}LOG_FILE\"")
appendLine()
appendLine(generateBinaryCheck(config.targetPath))
appendLine()
// 仅在支持隐藏挂载功能时执行相关配置
if (config.support158) {
// SUS挂载隐藏控制
val hideValue = if (config.hideSusMountsForAllProcs) 1 else 0
appendLine("# 设置SUS挂载隐藏控制")
appendLine("\"${'$'}SUSFS_BIN\" hide_sus_mnts_for_all_procs $hideValue")
appendLine("echo \"$(get_current_time): SUS挂载隐藏控制设置为: ${if (config.hideSusMountsForAllProcs) "对所有进程隐藏" else "仅对非KSU进程隐藏"}\" >> \"${'$'}LOG_FILE\"")
appendLine()
// 路径设置和SUS路径设置
if (config.susPaths.isNotEmpty() || config.susLoopPaths.isNotEmpty()) {
generatePathSettingSection(config.androidDataPath, config.sdcardPath)
appendLine()
// 添加普通SUS路径
if (config.susPaths.isNotEmpty()) {
generateSusPathsSection(config.susPaths)
}
// 添加循环SUS路径
if (config.susLoopPaths.isNotEmpty()) {
generateSusLoopPathsSection(config.susLoopPaths)
}
}
}
appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
@SuppressLint("SdCardPath")
private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) {
appendLine("# 路径配置")
appendLine("# 设置Android Data路径")
appendLine("until [ -d \"/sdcard/Android\" ]; do sleep 1; done")
appendLine("sleep 60")
appendLine()
appendLine("\"${'$'}SUSFS_BIN\" set_android_data_root_path '$androidDataPath'")
appendLine("echo \"$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"${'$'}LOG_FILE\"")
appendLine()
appendLine("# 设置SD卡路径")
appendLine("\"${'$'}SUSFS_BIN\" set_sdcard_root_path '$sdcardPath'")
appendLine("echo \"$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
/**
* 生成module.prop文件内容
*/
fun generateModuleProp(moduleId: String): String {
val moduleVersion = "v1.0.2"
val moduleVersionCode = "1002"
return """
id=$moduleId
name=SuSFS Manager
version=$moduleVersion
versionCode=$moduleVersionCode
author=ShirkNeko
description=SuSFS Manager Auto Configuration Module (自动生成请不要手动卸载或删除该模块! / Automatically generated Please do not manually uninstall or delete the module!)
updateJson=
""".trimIndent()
}
}

View File

@@ -3,7 +3,6 @@ package com.sukisu.ultra.ui.viewmodel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.SystemClock
import android.system.Os import android.system.Os
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -13,6 +12,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.dergoogler.mmrl.platform.Platform.Companion.context import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.sukisu.ultra.KernelVersion import com.sukisu.ultra.KernelVersion
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.getKernelVersion import com.sukisu.ultra.getKernelVersion
@@ -27,18 +27,20 @@ import androidx.core.content.edit
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
companion object { companion object {
private const val TAG = "HomeViewModel" private const val TAG = "HomeViewModel"
private const val CACHE_DURATION = 5 * 60 * 1000L
private const val PREFS_NAME = "home_cache" private const val PREFS_NAME = "home_cache"
private const val KEY_SYSTEM_STATUS = "system_status" private const val KEY_SYSTEM_STATUS = "system_status"
private const val KEY_SYSTEM_INFO = "system_info" private const val KEY_SYSTEM_INFO = "system_info"
private const val KEY_VERSION_INFO = "version_info" private const val KEY_VERSION_INFO = "version_info"
private const val KEY_LAST_UPDATE = "last_update_time" private const val KEY_LAST_UPDATE = "last_update_time"
private const val KEY_ERROR_COUNT = "error_count"
private const val MAX_ERROR_COUNT = 2
} }
// 系统状态 // 系统状态
data class SystemStatus( data class SystemStatus(
val isManager: Boolean = false, val isManager: Boolean = false,
val ksuVersion: Int? = null, val ksuVersion: Int? = null,
val ksuFullVersion : String? = null,
val lkmMode: Boolean? = null, val lkmMode: Boolean? = null,
val kernelVersion: KernelVersion = getKernelVersion(), val kernelVersion: KernelVersion = getKernelVersion(),
val isRootAvailable: Boolean = false, val isRootAvailable: Boolean = false,
@@ -61,7 +63,10 @@ class HomeViewModel : ViewModel() {
val susSUMode: String = "", val susSUMode: String = "",
val superuserCount: Int = 0, val superuserCount: Int = 0,
val moduleCount: Int = 0, val moduleCount: Int = 0,
val kpmModuleCount: Int = 0 val kpmModuleCount: Int = 0,
val managersList: Natives.ManagersList? = null,
val isDynamicSignEnabled: Boolean = false,
val zygiskImplement: String = ""
) )
private val gson = Gson() private val gson = Gson()
@@ -78,89 +83,175 @@ class HomeViewModel : ViewModel() {
var isSimpleMode by mutableStateOf(false) var isSimpleMode by mutableStateOf(false)
private set private set
var isKernelSimpleMode by mutableStateOf(false)
private set
var isHideVersion by mutableStateOf(false) var isHideVersion by mutableStateOf(false)
private set private set
var isHideOtherInfo by mutableStateOf(false) var isHideOtherInfo by mutableStateOf(false)
private set private set
var isHideSusfsStatus by mutableStateOf(false) var isHideSusfsStatus by mutableStateOf(false)
private set private set
var isHideZygiskImplement by mutableStateOf(false)
private set
var isHideLinkCard by mutableStateOf(false) var isHideLinkCard by mutableStateOf(false)
private set private set
var showKpmInfo by mutableStateOf(true) var showKpmInfo by mutableStateOf(false)
private set private set
private fun clearAllCache() {
try {
prefs.edit { clear() }
Log.i(TAG, "All cache cleared successfully")
} catch (e: Exception) {
Log.e(TAG, "Error clearing cache", e)
}
}
private fun resetToDefaults() {
systemStatus = SystemStatus()
systemInfo = SystemInfo()
latestVersionInfo = LatestVersionInfo()
isSimpleMode = false
isKernelSimpleMode = false
isHideVersion = false
isHideOtherInfo = false
isHideSusfsStatus = false
isHideZygiskImplement = false
isHideLinkCard = false
showKpmInfo = false
}
private fun handleError(error: Exception, operation: String) {
Log.e(TAG, "Error in $operation", error)
val errorCount = prefs.getInt(KEY_ERROR_COUNT, 0)
val newErrorCount = errorCount + 1
if (newErrorCount >= MAX_ERROR_COUNT) {
Log.w(TAG, "Too many errors ($newErrorCount), clearing cache and resetting")
clearAllCache()
resetToDefaults()
} else {
prefs.edit {
putInt(KEY_ERROR_COUNT, newErrorCount)
}
}
}
private fun String?.orSafe(default: String = ""): String {
return if (this.isNullOrBlank()) default else this
}
private fun <T, R> Pair<T?, R?>?.orSafe(default: Pair<T, R>): Pair<T, R> {
return if (this?.first == null || this.second == null) default else Pair(this.first!!, this.second!!)
}
fun loadUserSettings(context: Context) { fun loadUserSettings(context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) try {
isSimpleMode = prefs.getBoolean("is_simple_mode", false) val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
isHideVersion = prefs.getBoolean("is_hide_version", false) isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false)
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false) isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false)
isHideSusfsStatus = prefs.getBoolean("is_hide_susfs_status", false) isHideVersion = settingsPrefs.getBoolean("is_hide_version", false)
isHideLinkCard = prefs.getBoolean("is_hide_link_card", false) isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false)
showKpmInfo = prefs.getBoolean("show_kpm_info", true) isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false)
isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false)
isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false)
showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false)
} catch (e: Exception) {
handleError(e, "loadUserSettings")
}
} }
} }
fun initializeData() { fun initializeData() {
viewModelScope.launch { viewModelScope.launch {
val currentTime = System.currentTimeMillis() try {
val lastUpdateTime = prefs.getLong(KEY_LAST_UPDATE, 0)
val shouldRefresh = currentTime - lastUpdateTime > CACHE_DURATION
if (!shouldRefresh) {
loadCachedData() loadCachedData()
} else { // 成功加载后重置错误计数
fetchAndSaveData() prefs.edit {
putInt(KEY_ERROR_COUNT, 0)
}
} catch(e: Exception) {
handleError(e, "initializeData")
} }
} }
} }
private fun loadCachedData() { private fun loadCachedData() {
prefs.getString(KEY_SYSTEM_STATUS, null)?.let { try {
systemStatus = gson.fromJson(it, SystemStatus::class.java) prefs.getString(KEY_SYSTEM_STATUS, null)?.let { statusJson ->
} try {
prefs.getString(KEY_SYSTEM_INFO, null)?.let { val cachedStatus = gson.fromJson(statusJson, SystemStatus::class.java)
systemInfo = gson.fromJson(it, SystemInfo::class.java) if (cachedStatus != null) {
} systemStatus = cachedStatus
prefs.getString(KEY_VERSION_INFO, null)?.let { }
latestVersionInfo = gson.fromJson(it, LatestVersionInfo::class.java) } catch (e: JsonSyntaxException) {
Log.w(TAG, "Invalid system status JSON, using defaults", e)
}
}
prefs.getString(KEY_SYSTEM_INFO, null)?.let { infoJson ->
try {
val cachedInfo = gson.fromJson(infoJson, SystemInfo::class.java)
if (cachedInfo != null) {
systemInfo = cachedInfo
}
} catch (e: JsonSyntaxException) {
Log.w(TAG, "Invalid system info JSON, using defaults", e)
}
}
prefs.getString(KEY_VERSION_INFO, null)?.let { versionJson ->
try {
val cachedVersion = gson.fromJson(versionJson, LatestVersionInfo::class.java)
if (cachedVersion != null) {
latestVersionInfo = cachedVersion
}
} catch (e: JsonSyntaxException) {
Log.w(TAG, "Invalid version info JSON, using defaults", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error loading cached data", e)
throw e
} }
} }
private suspend fun fetchAndSaveData() { private suspend fun fetchAndSaveData() {
fetchSystemStatus() try {
fetchSystemInfo() fetchSystemStatus()
withContext(Dispatchers.IO) { fetchSystemInfo()
prefs.edit { withContext(Dispatchers.IO) {
putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus)) prefs.edit {
putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo)) putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus))
putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo)) putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo))
putLong(KEY_LAST_UPDATE, System.currentTimeMillis()) putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo))
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
putInt(KEY_ERROR_COUNT, 0)
}
} }
} catch (e: Exception) {
handleError(e, "fetchAndSaveData")
} }
} }
fun checkForUpdates(context: Context) { fun checkForUpdates(context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true) val checkUpdate = settingsPrefs.getBoolean("check_update", true)
if (checkUpdate) { if (checkUpdate) {
val currentTime = System.currentTimeMillis() val newVersionInfo = checkNewVersion()
val lastUpdateTime = prefs.getLong(KEY_LAST_UPDATE, 0) latestVersionInfo = newVersionInfo
val shouldRefresh = currentTime - lastUpdateTime > CACHE_DURATION prefs.edit {
putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo))
if (shouldRefresh) { putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
val start = SystemClock.elapsedRealtime()
val newVersionInfo = checkNewVersion()
latestVersionInfo = newVersionInfo
prefs.edit { putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo)) }
Log.i(TAG, "Update check completed in ${SystemClock.elapsedRealtime() - start}ms")
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error checking for updates", e) handleError(e, "checkForUpdates")
} }
} }
} }
@@ -171,7 +262,7 @@ class HomeViewModel : ViewModel() {
fetchAndSaveData() fetchAndSaveData()
checkForUpdates(context) checkForUpdates(context)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error refreshing data", e) handleError(e, "refreshAllData")
} }
} }
} }
@@ -180,23 +271,97 @@ class HomeViewModel : ViewModel() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val kernelVersion = getKernelVersion() val kernelVersion = getKernelVersion()
val isManager = Natives.becomeManager(ksuApp.packageName) val isManager = try {
val ksuVersion = if (isManager) Natives.version else null Natives.becomeManager(ksuApp.packageName.orSafe("com.sukisu.ultra"))
} catch (e: Exception) {
Log.w(TAG, "Failed to become manager", e)
false
}
val ksuVersion = if (isManager) {
try {
Natives.version
} catch (e: Exception) {
Log.w(TAG, "Failed to get KSU version", e)
null
}
} else null
val fullVersion = try {
Natives.getFullVersion().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get full version", e)
"Unknown"
}
val ksuFullVersion = if (isKernelSimpleMode) {
try {
val startIndex = fullVersion.indexOf('v')
if (startIndex >= 0) {
val endIndex = fullVersion.indexOf('-', startIndex)
val versionStr = if (endIndex > startIndex) {
fullVersion.substring(startIndex, endIndex)
} else {
fullVersion.substring(startIndex)
}
val numericVersion = "v" + (Regex("""\d+(\.\d+)*""").find(versionStr)?.value ?: versionStr)
numericVersion
} else {
fullVersion
}
} catch (e: Exception) {
Log.w(TAG, "Failed to process full version", e)
fullVersion
}
} else {
fullVersion
}
val lkmMode = ksuVersion?.let { val lkmMode = ksuVersion?.let {
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null try {
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) {
Natives.isLkmMode
} else null
} catch (e: Exception) {
Log.w(TAG, "Failed to get LKM mode", e)
null
}
}
val isRootAvailable = try {
rootAvailable()
} catch (e: Exception) {
Log.w(TAG, "Failed to check root availability", e)
false
}
val isKpmConfigured = try {
Natives.isKPMEnabled()
} catch (e: Exception) {
Log.w(TAG, "Failed to check KPM status", e)
false
}
val requireNewKernel = try {
isManager && Natives.requireNewKernel()
} catch (e: Exception) {
Log.w(TAG, "Failed to check kernel requirement", e)
false
} }
systemStatus = SystemStatus( systemStatus = SystemStatus(
isManager = isManager, isManager = isManager,
ksuVersion = ksuVersion, ksuVersion = ksuVersion,
ksuFullVersion = ksuFullVersion,
lkmMode = lkmMode, lkmMode = lkmMode,
kernelVersion = kernelVersion, kernelVersion = kernelVersion,
isRootAvailable = rootAvailable(), isRootAvailable = isRootAvailable,
isKpmConfigured = Natives.isKPMEnabled(), isKpmConfigured = isKpmConfigured,
requireNewKernel = isManager && Natives.requireNewKernel() requireNewKernel = requireNewKernel
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error fetching system status", e) Log.e(TAG, "Error fetching system status", e)
throw e
} }
} }
} }
@@ -205,52 +370,190 @@ class HomeViewModel : ViewModel() {
private suspend fun fetchSystemInfo() { private suspend fun fetchSystemInfo() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {
val uname = Os.uname() val uname = try {
val kpmVersion = getKpmVersion() Os.uname()
val suSFS = getSuSFS() } catch (e: Exception) {
Log.w(TAG, "Failed to get uname", e)
null
}
val kpmVersion = try {
getKpmVersion().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get kpm version", e)
"Unknown"
}
val suSFS = try {
getSuSFS().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS", e)
"Unknown"
}
var suSFSVersion = "" var suSFSVersion = ""
var suSFSVariant = "" var suSFSVariant = ""
var suSFSFeatures = "" var suSFSFeatures = ""
var susSUMode = "" var susSUMode = ""
if (suSFS == "Supported") { if (suSFS == "Supported") {
suSFSVersion = getSuSFSVersion() suSFSVersion = try {
getSuSFSVersion().orSafe("")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS version", e)
""
}
if (suSFSVersion.isNotEmpty()) { if (suSFSVersion.isNotEmpty()) {
suSFSVariant = getSuSFSVariant() suSFSVariant = try {
suSFSFeatures = getSuSFSFeatures() getSuSFSVariant().orSafe("")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS variant", e)
""
}
suSFSFeatures = try {
getSuSFSFeatures().orSafe("")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SuSFS features", e)
""
}
val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU" val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
if (isSUS_SU) { if (isSUS_SU) {
susSUMode = try { susSUMode = try {
susfsSUS_SU_Mode().toString() susfsSUS_SU_Mode()
} catch (_: Exception) { } catch (e: Exception) {
Log.w(TAG, "Failed to get SUS SU mode", e)
"" ""
} }
} }
} }
} }
// 获取动态管理器状态和管理器列表
val dynamicSignConfig = try {
Natives.getDynamicManager()
} catch (e: Exception) {
Log.w(TAG, "Failed to get dynamic manager config", e)
null
}
val isDynamicSignEnabled = try {
dynamicSignConfig?.isValid() == true
} catch (e: Exception) {
Log.w(TAG, "Failed to check dynamic manager validity", e)
false
}
val managersList = if (isDynamicSignEnabled) {
try {
Natives.getManagersList()
} catch (e: Exception) {
Log.w(TAG, "Failed to get managers list", e)
null
}
} else {
null
}
val deviceModel = try {
getDeviceModel().orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get device model", e)
"Unknown"
}
val managerVersion = try {
getManagerVersion(ksuApp.applicationContext).orSafe(Pair("Unknown", 0L))
} catch (e: Exception) {
Log.w(TAG, "Failed to get manager version", e)
Pair("Unknown", 0L)
}
val seLinuxStatus = try {
getSELinuxStatus(context).orSafe("Unknown")
} catch (e: Exception) {
Log.w(TAG, "Failed to get SELinux status", e)
"Unknown"
}
val superuserCount = try {
getSuperuserCount()
} catch (e: Exception) {
Log.w(TAG, "Failed to get superuser count", e)
0
}
val moduleCount = try {
getModuleCount()
} catch (e: Exception) {
Log.w(TAG, "Failed to get module count", e)
0
}
val kpmModuleCount = try {
getKpmModuleCount()
} catch (e: Exception) {
Log.w(TAG, "Failed to get kpm module count", e)
0
}
val zygiskImplement = try {
getZygiskImplement().orSafe("None")
} catch (e: Exception) {
Log.w(TAG, "Failed to get Zygisk implement", e)
"None"
}
systemInfo = SystemInfo( systemInfo = SystemInfo(
kernelRelease = uname.release, kernelRelease = uname?.release.orSafe("Unknown"),
androidVersion = Build.VERSION.RELEASE, androidVersion = Build.VERSION.RELEASE.orSafe("Unknown"),
deviceModel = getDeviceModel(), deviceModel = deviceModel,
managerVersion = getManagerVersion(ksuApp.applicationContext), managerVersion = managerVersion,
seLinuxStatus = getSELinuxStatus(context), seLinuxStatus = seLinuxStatus,
kpmVersion = kpmVersion, kpmVersion = kpmVersion,
suSFSStatus = suSFS, suSFSStatus = suSFS,
suSFSVersion = suSFSVersion, suSFSVersion = suSFSVersion,
suSFSVariant = suSFSVariant, suSFSVariant = suSFSVariant,
suSFSFeatures = suSFSFeatures, suSFSFeatures = suSFSFeatures,
susSUMode = susSUMode, susSUMode = susSUMode,
superuserCount = getSuperuserCount(), superuserCount = superuserCount,
moduleCount = getModuleCount(), moduleCount = moduleCount,
kpmModuleCount = getKpmModuleCount() kpmModuleCount = kpmModuleCount,
managersList = managersList,
isDynamicSignEnabled = isDynamicSignEnabled,
zygiskImplement = zygiskImplement
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error fetching system info", e) Log.e(TAG, "Error fetching system info", e)
throw e
} }
} }
} }
private fun getDeviceInfo(): String {
return try {
var manufacturer = Build.MANUFACTURER.orSafe("Unknown")
manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1)
val brand = Build.BRAND.orSafe("")
if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) {
manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1)
}
val model = Build.MODEL.orSafe("")
if (model.isNotEmpty()) {
manufacturer += " $model "
}
manufacturer
} catch (e: Exception) {
Log.w(TAG, "Failed to get device info", e)
"Unknown Device"
}
}
@SuppressLint("PrivateApi") @SuppressLint("PrivateApi")
private fun getDeviceModel(): String { private fun getDeviceModel(): String {
return try { return try {
@@ -262,29 +565,34 @@ class HomeViewModel : ViewModel() {
"ro.vivo.market.name", // Vivo "ro.vivo.market.name", // Vivo
"ro.config.marketing_name" // Huawei "ro.config.marketing_name" // Huawei
) )
var result = Build.DEVICE var result = getDeviceInfo()
for (key in marketNameKeys) { for (key in marketNameKeys) {
val marketName = getMethod.invoke(null, key, "") as String try {
if (marketName.isNotEmpty()) { val marketName = getMethod.invoke(null, key, "") as String
result = marketName if (marketName.isNotEmpty()) {
break result = marketName
break
}
} catch (e: Exception) {
Log.w(TAG, "Failed to get market name for key: $key", e)
} }
} }
result result
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error getting device model", e) Log.w(TAG, "Error getting device model", e)
Build.DEVICE getDeviceInfo()
} }
} }
private fun getManagerVersion(context: Context): Pair<String, Long> { private fun getManagerVersion(context: Context): Pair<String, Long> {
return try { return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!! val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo) val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo)
Pair(packageInfo.versionName!!, versionCode) val versionName = packageInfo.versionName.orSafe("Unknown")
Pair(versionName, versionCode)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error getting manager version", e) Log.w(TAG, "Error getting manager version", e)
Pair("", 0L) Pair("Unknown", 0L)
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui.viewmodel package com.sukisu.ultra.ui.viewmodel
import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -14,12 +15,18 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.listModules import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.getRootShell
import com.sukisu.ultra.ui.util.ModuleVerificationManager
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.text.Collator import java.text.Collator
import java.text.DecimalFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.math.log10
import kotlin.math.pow
import androidx.core.content.edit
/** /**
* @author ShirkNeko * @author ShirkNeko
@@ -33,6 +40,38 @@ class ModuleViewModel : ViewModel() {
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0" private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
} }
// 模块大小缓存管理器
private lateinit var moduleSizeCache: ModuleSizeCache
fun initializeCache(context: Context) {
if (!::moduleSizeCache.isInitialized) {
moduleSizeCache = ModuleSizeCache(context)
}
}
fun getModuleSize(dirId: String): String {
if (!::moduleSizeCache.isInitialized) {
return "0 KB"
}
val size = moduleSizeCache.getModuleSize(dirId)
return formatFileSize(size)
}
/**
* 刷新所有模块的大小缓存
* 只在安装、卸载、更新模块后调用
*/
fun refreshModuleSizeCache() {
if (!::moduleSizeCache.isInitialized) return
viewModelScope.launch(Dispatchers.IO) {
Log.d(TAG, "开始刷新模块大小缓存")
val currentModules = modules.map { it.dirId }
moduleSizeCache.refreshCache(currentModules)
Log.d(TAG, "模块大小缓存刷新完成")
}
}
class ModuleInfo( class ModuleInfo(
val id: String, val id: String,
val name: String, val name: String,
@@ -48,6 +87,8 @@ class ModuleViewModel : ViewModel() {
val hasActionScript: Boolean, val hasActionScript: Boolean,
val dirId: String, // real module id (dir name) val dirId: String, // real module id (dir name)
var config: ModuleConfig? = null, var config: ModuleConfig? = null,
var isVerified: Boolean = false, // 添加验证状态字段
var verificationTimestamp: Long = 0L, // 添加验证时间戳
) )
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
@@ -75,6 +116,8 @@ class ModuleViewModel : ViewModel() {
fun markNeedRefresh() { fun markNeedRefresh() {
isNeedRefresh = true isNeedRefresh = true
// 标记需要刷新时,同时刷新大小缓存
refreshModuleSizeCache()
} }
fun fetchModuleList() { fun fetchModuleList() {
@@ -91,7 +134,7 @@ class ModuleViewModel : ViewModel() {
Log.i(TAG, "result: $result") Log.i(TAG, "result: $result")
val array = JSONArray(result) val array = JSONArray(result)
modules = (0 until array.length()) val moduleInfos = (0 until array.length())
.asSequence() .asSequence()
.map { array.getJSONObject(it) } .map { array.getJSONObject(it) }
.map { obj -> .map { obj ->
@@ -111,6 +154,26 @@ class ModuleViewModel : ViewModel() {
obj.getString("dir_id") obj.getString("dir_id")
) )
}.toList() }.toList()
// 批量检查所有模块的验证状态
val moduleIds = moduleInfos.map { it.dirId }
val verificationStatus = ModuleVerificationManager.batchCheckVerificationStatus(moduleIds)
// 更新模块验证状态
modules = moduleInfos.map { moduleInfo ->
val isVerified = verificationStatus[moduleInfo.dirId] ?: false
val verificationTimestamp = if (isVerified) {
ModuleVerificationManager.getVerificationTimestamp(moduleInfo.dirId)
} else {
0L
}
moduleInfo.copy(
isVerified = isVerified,
verificationTimestamp = verificationTimestamp
)
}
launch { launch {
modules.forEach { module -> modules.forEach { module ->
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
@@ -144,6 +207,13 @@ class ModuleViewModel : ViewModel() {
} }
} }
} }
// 首次加载模块列表时,初始化缓存
if (::moduleSizeCache.isInitialized) {
val currentModules = modules.map { it.dirId }
moduleSizeCache.initializeCacheIfNeeded(currentModules)
}
isNeedRefresh = false isNeedRefresh = false
}.onFailure { e -> }.onFailure { e ->
Log.e(TAG, "fetchModuleList: ", e) Log.e(TAG, "fetchModuleList: ", e)
@@ -220,4 +290,195 @@ class ModuleViewModel : ViewModel() {
return Triple(zipUrl, version, changelog) return Triple(zipUrl, version, changelog)
} }
}
fun ModuleViewModel.ModuleInfo.copy(
id: String = this.id,
name: String = this.name,
author: String = this.author,
version: String = this.version,
versionCode: Int = this.versionCode,
description: String = this.description,
enabled: Boolean = this.enabled,
update: Boolean = this.update,
remove: Boolean = this.remove,
updateJson: String = this.updateJson,
hasWebUi: Boolean = this.hasWebUi,
hasActionScript: Boolean = this.hasActionScript,
dirId: String = this.dirId,
config: ModuleConfig? = this.config,
isVerified: Boolean = this.isVerified,
verificationTimestamp: Long = this.verificationTimestamp
): ModuleViewModel.ModuleInfo {
return ModuleViewModel.ModuleInfo(
id, name, author, version, versionCode, description,
enabled, update, remove, updateJson, hasWebUi, hasActionScript,
dirId, config, isVerified, verificationTimestamp
)
}
/**
* 模块大小缓存管理器
*/
class ModuleSizeCache(context: Context) {
companion object {
private const val TAG = "ModuleSizeCache"
private const val CACHE_PREFS_NAME = "module_size_cache"
private const val CACHE_VERSION_KEY = "cache_version"
private const val CACHE_INITIALIZED_KEY = "cache_initialized"
private const val CURRENT_CACHE_VERSION = 1
}
private val cachePrefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
private val sizeCache = mutableMapOf<String, Long>()
init {
loadCacheFromPrefs()
}
/**
* 从SharedPreferences加载缓存
*/
private fun loadCacheFromPrefs() {
try {
val cacheVersion = cachePrefs.getInt(CACHE_VERSION_KEY, 0)
if (cacheVersion != CURRENT_CACHE_VERSION) {
Log.d(TAG, "缓存版本不匹配,清空缓存")
clearCache()
return
}
val allEntries = cachePrefs.all
for ((key, value) in allEntries) {
if (key != CACHE_VERSION_KEY && key != CACHE_INITIALIZED_KEY && value is Long) {
sizeCache[key] = value
}
}
Log.d(TAG, "从缓存加载了 ${sizeCache.size} 个模块大小数据")
} catch (e: Exception) {
Log.e(TAG, "加载缓存失败", e)
clearCache()
}
}
/**
* 保存缓存到SharedPreferences
*/
private fun saveCacheToPrefs() {
try {
cachePrefs.edit {
putInt(CACHE_VERSION_KEY, CURRENT_CACHE_VERSION)
putBoolean(CACHE_INITIALIZED_KEY, true)
for ((dirId, size) in sizeCache) {
putLong(dirId, size)
}
}
Log.d(TAG, "保存了 ${sizeCache.size} 个模块大小到缓存")
} catch (e: Exception) {
Log.e(TAG, "保存缓存失败", e)
}
}
/**
* 获取模块大小(从缓存)
*/
fun getModuleSize(dirId: String): Long {
return sizeCache[dirId] ?: 0L
}
/**
* 检查缓存是否已初始化,如果没有则初始化
*/
fun initializeCacheIfNeeded(currentModules: List<String>) {
val isInitialized = cachePrefs.getBoolean(CACHE_INITIALIZED_KEY, false)
if (!isInitialized || sizeCache.isEmpty()) {
Log.d(TAG, "首次初始化缓存,计算所有模块大小")
refreshCache(currentModules)
} else {
// 检查是否有新模块需要计算大小
val newModules = currentModules.filter { !sizeCache.containsKey(it) }
if (newModules.isNotEmpty()) {
Log.d(TAG, "发现 ${newModules.size} 个新模块,计算大小: $newModules")
for (dirId in newModules) {
val size = calculateModuleFolderSize(dirId)
sizeCache[dirId] = size
Log.d(TAG, "新模块 $dirId 大小: ${formatFileSize(size)}")
}
saveCacheToPrefs()
}
}
}
/**
* 刷新所有模块的大小缓存
*/
fun refreshCache(currentModules: List<String>) {
try {
// 清理不存在的模块缓存
val toRemove = sizeCache.keys.filter { it !in currentModules }
toRemove.forEach { sizeCache.remove(it) }
if (toRemove.isNotEmpty()) {
Log.d(TAG, "清理了 ${toRemove.size} 个不存在的模块缓存: $toRemove")
}
// 计算所有当前模块的大小
for (dirId in currentModules) {
val size = calculateModuleFolderSize(dirId)
sizeCache[dirId] = size
Log.d(TAG, "更新模块 $dirId 大小: ${formatFileSize(size)}")
}
// 保存到持久化存储
saveCacheToPrefs()
} catch (e: Exception) {
Log.e(TAG, "刷新缓存失败", e)
}
}
/**
* 清空所有缓存
*/
private fun clearCache() {
sizeCache.clear()
cachePrefs.edit { clear() }
Log.d(TAG, "清空所有缓存")
}
/**
* 实际计算模块文件夹大小
*/
private fun calculateModuleFolderSize(dirId: String): Long {
return try {
val shell = getRootShell()
val command = "du -sb /data/adb/modules/$dirId"
val result = shell.newJob().add(command).to(ArrayList(), null).exec()
if (result.isSuccess && result.out.isNotEmpty()) {
val sizeStr = result.out.firstOrNull()?.split("\t")?.firstOrNull()
sizeStr?.toLongOrNull() ?: 0L
} else {
0L
}
} catch (e: Exception) {
Log.e(TAG, "计算模块大小失败 $dirId: ${e.message}")
0L
}
}
}
/**
* 格式化文件大小的工具函数
*/
fun formatFileSize(bytes: Long): String {
if (bytes <= 0) return "0 KB"
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
return DecimalFormat("#,##0.#").format(
bytes / 1024.0.pow(digitGroups.toDouble())
) + " " + units[digitGroups]
} }

View File

@@ -1,40 +1,93 @@
package com.sukisu.ultra.ui.viewmodel package com.sukisu.ultra.ui.viewmodel
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.IBinder
import android.os.Parcelable import android.os.Parcelable
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
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.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.HanziToPinyin
import java.text.Collator import java.text.Collator
import java.util.* import java.util.*
import com.dergoogler.mmrl.platform.Platform import java.util.concurrent.ThreadPoolExecutor
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS import java.util.concurrent.TimeUnit
import com.sukisu.ultra.ui.webui.getInstalledPackagesAll import java.util.concurrent.LinkedBlockingQueue
import kotlinx.coroutines.delay import androidx.core.content.edit
import kotlinx.coroutines.withTimeoutOrNull import com.sukisu.ultra.ui.KsuService
import com.sukisu.ultra.ui.util.KsuCli
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.asCoroutineDispatcher
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
// 应用分类
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"),
CUSTOM(com.sukisu.ultra.R.string.category_custom_apps, "CUSTOM"),
DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT");
companion object {
fun fromPersistKey(key: String): AppCategory {
return entries.find { it.persistKey == key } ?: ALL
}
}
}
// 排序方式
enum class SortType(val displayNameRes: Int, val persistKey: String) {
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"),
INSTALL_TIME_NEW(com.sukisu.ultra.R.string.sort_install_time_new, "INSTALL_TIME_NEW"),
INSTALL_TIME_OLD(com.sukisu.ultra.R.string.sort_install_time_old, "INSTALL_TIME_OLD"),
SIZE_DESC(com.sukisu.ultra.R.string.sort_size_desc, "SIZE_DESC"),
SIZE_ASC(com.sukisu.ultra.R.string.sort_size_asc, "SIZE_ASC"),
USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ");
companion object {
fun fromPersistKey(key: String): SortType {
return entries.find { it.persistKey == key } ?: NAME_ASC
}
}
}
/** /**
* @author ShirkNeko * @author ShirkNeko
* @date 2025/5/31. * @date 2025/5/31.
*/ */
class SuperUserViewModel : ViewModel() { class SuperUserViewModel : ViewModel() {
val isPlatformAlive get() = Platform.isAlive
companion object { companion object {
private const val TAG = "SuperUserViewModel" private const val TAG = "SuperUserViewModel"
var apps by mutableStateOf<List<AppInfo>>(emptyList()) var apps by mutableStateOf<List<AppInfo>>(emptyList())
private const val PREFS_NAME = "settings"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_SELECTED_CATEGORY = "selected_category"
private const val KEY_CURRENT_SORT_TYPE = "current_sort_type"
private const val CORE_POOL_SIZE = 8
private const val MAX_POOL_SIZE = 16
private const val KEEP_ALIVE_TIME = 60L
private const val BATCH_SIZE = 20
} }
@Parcelize @Parcelize
@@ -63,8 +116,35 @@ class SuperUserViewModel : ViewModel() {
} }
} }
private val appProcessingThreadPool = ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
LinkedBlockingQueue()
) { runnable ->
Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply {
isDaemon = true
priority = Thread.NORM_PRIORITY
}
}.asCoroutineDispatcher()
private val appListMutex = Mutex()
private val configChangeListeners = mutableSetOf<(String) -> Unit>()
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
var search by mutableStateOf("") var search by mutableStateOf("")
var showSystemApps by mutableStateOf(false)
var showSystemApps by mutableStateOf(loadShowSystemApps())
private set
var selectedCategory by mutableStateOf(loadSelectedCategory())
private set
var currentSortType by mutableStateOf(loadCurrentSortType())
private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
@@ -74,6 +154,96 @@ class SuperUserViewModel : ViewModel() {
var selectedApps by mutableStateOf<Set<String>>(emptySet()) var selectedApps by mutableStateOf<Set<String>>(emptySet())
internal set internal set
// 加载进度状态
var loadingProgress by mutableFloatStateOf(0f)
private set
var loadingMessage by mutableStateOf("")
private set
/**
* 从SharedPreferences加载显示系统应用设置
*/
private fun loadShowSystemApps(): Boolean {
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
}
/**
* 从SharedPreferences加载选择的应用分类
*/
private fun loadSelectedCategory(): AppCategory {
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey
return AppCategory.fromPersistKey(categoryKey)
}
/**
* 从SharedPreferences加载当前排序方式
*/
private fun loadCurrentSortType(): SortType {
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey
return SortType.fromPersistKey(sortKey)
}
/**
* 更新显示系统应用设置并保存到SharedPreferences
*/
fun updateShowSystemApps(newValue: Boolean) {
showSystemApps = newValue
saveShowSystemApps(newValue)
notifyAppListChanged()
}
private fun notifyAppListChanged() {
val currentApps = apps
apps = emptyList()
apps = currentApps
}
/**
* 更新选择的应用分类并保存到SharedPreferences
*/
fun updateSelectedCategory(newCategory: AppCategory) {
selectedCategory = newCategory
saveSelectedCategory(newCategory)
}
/**
* 更新当前排序方式并保存到SharedPreferences
*/
fun updateCurrentSortType(newSortType: SortType) {
currentSortType = newSortType
saveCurrentSortType(newSortType)
}
/**
* 保存显示系统应用设置到SharedPreferences
*/
private fun saveShowSystemApps(value: Boolean) {
prefs.edit {
putBoolean(KEY_SHOW_SYSTEM_APPS, value)
}
Log.d(TAG, "Saved show system apps: $value")
}
/**
* 保存选择的应用分类到SharedPreferences
*/
private fun saveSelectedCategory(category: AppCategory) {
prefs.edit {
putString(KEY_SELECTED_CATEGORY, category.persistKey)
}
Log.d(TAG, "Saved selected category: ${category.persistKey}")
}
/**
* 保存当前排序方式到SharedPreferences
*/
private fun saveCurrentSortType(sortType: SortType) {
prefs.edit {
putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey)
}
Log.d(TAG, "Saved current sort type: ${sortType.persistKey}")
}
private val sortedList by derivedStateOf { private val sortedList by derivedStateOf {
val comparator = compareBy<AppInfo> { val comparator = compareBy<AppInfo> {
when { when {
@@ -88,7 +258,7 @@ class SuperUserViewModel : ViewModel() {
} }
val appList by derivedStateOf { val appList by derivedStateOf {
sortedList.filter { val filtered = sortedList.filter {
it.label.contains(search, true) || it.packageName.contains( it.label.contains(search, true) || it.packageName.contains(
search, search,
true true
@@ -97,6 +267,8 @@ class SuperUserViewModel : ViewModel() {
}.filter { }.filter {
it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
} }
filtered
} }
// 切换批量操作模式 // 切换批量操作模式
@@ -129,19 +301,14 @@ class SuperUserViewModel : ViewModel() {
val profile = Natives.getAppProfile(packageName, it.uid) val profile = Natives.getAppProfile(packageName, it.uid)
val updatedProfile = profile.copy(allowSu = allowSu) val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) { if (Natives.setAppProfile(updatedProfile)) {
apps = apps.map { app -> updateAppProfileLocally(packageName, updatedProfile)
if (app.packageName == packageName) { notifyConfigChange(packageName)
app.copy(profile = updatedProfile)
} else {
app
}
}
} }
} }
} }
clearSelection() clearSelection()
showBatchActions = false // 批量操作完成后退出批量模式 showBatchActions = false
fetchAppList() // 刷新列表以显示最新状态 refreshAppConfigurations()
} }
// 批量更新权限和umount模块设置 // 批量更新权限和umount模块设置
@@ -156,6 +323,21 @@ class SuperUserViewModel : ViewModel() {
nonRootUseDefault = false nonRootUseDefault = false
) )
if (Natives.setAppProfile(updatedProfile)) { if (Natives.setAppProfile(updatedProfile)) {
updateAppProfileLocally(packageName, updatedProfile)
notifyConfigChange(packageName)
}
}
}
clearSelection()
showBatchActions = false
refreshAppConfigurations()
}
// 更新本地应用配置
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
appListMutex.tryLock().let { locked ->
if (locked) {
try {
apps = apps.map { app -> apps = apps.map { app ->
if (app.packageName == packageName) { if (app.packageName == packageName) {
app.copy(profile = updatedProfile) app.copy(profile = updatedProfile)
@@ -163,52 +345,175 @@ class SuperUserViewModel : ViewModel() {
app app
} }
} }
} finally {
appListMutex.unlock()
} }
} }
} }
clearSelection()
showBatchActions = false // 批量操作完成后退出批量模式
fetchAppList() // 刷新列表以显示最新状态
} }
// 更新本地应用配置 private fun notifyConfigChange(packageName: String) {
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) { configChangeListeners.forEach { listener ->
apps = apps.map { app -> try {
if (app.packageName == packageName) { listener(packageName)
app.copy(profile = updatedProfile) } catch (e: Exception) {
} else { Log.e(TAG, "Error notifying config change for $packageName", e)
app }
}
}
/**
* 刷新应用配置状态
*/
suspend fun refreshAppConfigurations() {
withContext(appProcessingThreadPool) {
supervisorScope {
val currentApps = apps.toList()
val batches = currentApps.chunked(BATCH_SIZE)
loadingProgress = 0f
val updatedApps = batches.mapIndexed { batchIndex, batch ->
async {
val batchResult = batch.map { app ->
try {
val updatedProfile = Natives.getAppProfile(app.packageName, app.uid)
app.copy(profile = updatedProfile)
} catch (e: Exception) {
Log.e(TAG, "Error refreshing profile for ${app.packageName}", e)
app
}
}
val progress = (batchIndex + 1).toFloat() / batches.size
loadingProgress = progress
batchResult
}
}.awaitAll().flatten()
appListMutex.withLock {
apps = updatedApps
}
loadingProgress = 1f
Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps")
}
}
}
private var serviceConnection: ServiceConnection? = null
private suspend fun connectKsuService(
onDisconnect: () -> Unit = {}
): IBinder? = suspendCoroutine { continuation ->
val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
onDisconnect()
serviceConnection = null
}
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
continuation.resume(binder)
}
}
serviceConnection = connection
val intent = Intent(ksuApp, KsuService::class.java)
try {
val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask(
intent,
Shell.EXECUTOR,
connection
)
val shell = KsuCli.SHELL
task?.let { shell.execTask(it) }
} catch (e: Exception) {
Log.e(TAG, "Failed to bind KsuService", e)
continuation.resume(null)
}
}
private fun stopKsuService() {
serviceConnection?.let { connection ->
try {
val intent = Intent(ksuApp, KsuService::class.java)
com.topjohnwu.superuser.ipc.RootService.stop(intent)
serviceConnection = null
} catch (e: Exception) {
Log.e(TAG, "Failed to stop KsuService", e)
} }
} }
} }
suspend fun fetchAppList() { suspend fun fetchAppList() {
isRefreshing = true isRefreshing = true
loadingProgress = 0f
val result = connectKsuService {
Log.w(TAG, "KsuService disconnected")
}
if (result == null) {
Log.e(TAG, "Failed to connect to KsuService")
isRefreshing = false
return
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
withTimeoutOrNull(TIMEOUT_MILLIS) {
while (!isPlatformAlive) {
delay(500)
}
} ?: return@withContext // Exit early if timeout
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val packages = Platform.getInstalledPackagesAll { try {
Log.e(TAG, "getInstalledPackagesAll:", it) val service = KsuService.Stub.asInterface(result)
Toast.makeText(ksuApp, "Something went wrong, check logs", Toast.LENGTH_SHORT).show() val allPackages = service?.getPackages(0)
withContext(Dispatchers.Main) {
stopKsuService()
}
loadingProgress = 0.3f
val packages = allPackages?.list ?: emptyList()
apps = packages.map { packageInfo ->
val appInfo = packageInfo.applicationInfo!!
val uid = appInfo.uid
val profile = Natives.getAppProfile(packageInfo.packageName, uid)
AppInfo(
label = appInfo.loadLabel(pm).toString(),
packageInfo = packageInfo,
profile = profile,
)
}.filter { it.packageName != ksuApp.packageName }
loadingProgress = 1f
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}")
} catch (e: Exception) {
Log.e(TAG, "Error fetching app list", e)
withContext(Dispatchers.Main) {
stopKsuService()
}
} finally {
isRefreshing = false
loadingProgress = 0f
loadingMessage = ""
} }
apps = packages.map { }
val appInfo = it.applicationInfo }
val uid = appInfo!!.uid /**
val profile = Natives.getAppProfile(it.packageName, uid) * 清理资源
AppInfo( */
label = appInfo.loadLabel(pm).toString(), override fun onCleared() {
packageInfo = it, super.onCleared()
profile = profile, try {
) stopKsuService()
}.filter { it.packageName != ksuApp.packageName } appProcessingThreadPool.close()
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") configChangeListeners.clear()
} catch (e: Exception) {
Log.e(TAG, "Error cleaning up resources", e)
} }
} }
} }

View File

@@ -41,10 +41,10 @@ class WebUIActivity : ComponentActivity() {
val name = intent.getStringExtra("name")!! val name = intent.getStringExtra("name")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name")) setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name"))
} else { } else {
val taskDescription = val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build() ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build()
setTaskDescription(taskDescription) setTaskDescription(taskDescription)
} }

View File

@@ -42,7 +42,7 @@ class WebUIXActivity : ComponentActivity() {
val osVersion = Build.VERSION.RELEASE val osVersion = Build.VERSION.RELEASE
val deviceModel = Build.MODEL val deviceModel = Build.MODEL
return "SukiSU /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)" return "SukiSU-Ultra /$ksuVersion (Linux; Android $osVersion; $deviceModel; $platform/$platformVersion)"
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -59,10 +59,10 @@ class WebUIXActivity : ComponentActivity() {
val name = intent.getStringExtra("name")!! val name = intent.getStringExtra("name")!!
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name")) setTaskDescription(ActivityManager.TaskDescription("SukiSU-Ultra - $name"))
} else { } else {
val taskDescription = val taskDescription =
ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build() ActivityManager.TaskDescription.Builder().setLabel("SukiSU-Ultra - $name").build()
setTaskDescription(taskDescription) setTaskDescription(taskDescription)
} }

View File

@@ -19,10 +19,12 @@ public class UltraShellHelper {
} }
public static boolean isPathExists(String path) { public static boolean isPathExists(String path) {
return runCmd("file " + path).contains("No such file or directory"); String result = runCmd("test -f '" + path + "' && echo 'exists'");
return result.contains("exists");
} }
public static void CopyFileTo(String path, String target) { public static boolean CopyFileTo(String path, String target) {
runCmd("cp -f " + path + " " + target); String result = runCmd("cp -f '" + path + "' '" + target + "' 2>&1");
return !result.contains("cp: ");
} }
} }

View File

@@ -0,0 +1,222 @@
package zako.zako.zako.zakoui.activity.component
import android.annotation.SuppressLint
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.navigation.NavHostController
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import com.ramcosta.composedestinations.spec.RouteOrDirection
import com.ramcosta.composedestinations.generated.NavGraphs
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.MainActivity
import zako.zako.zako.zakoui.activity.util.AppData
import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse
import com.sukisu.ultra.ui.screen.BottomBarDestination
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.navigationBars
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
@SuppressLint("ContextCastToActivity")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isFullFeatured = AppData.isFullFeatured(ksuApp.packageName)
val kpmVersion = getKpmVersionUse()
val cardColor = MaterialTheme.colorScheme.surfaceContainer
val activity = LocalContext.current as MainActivity
val settings by activity.settingsStateFlow.collectAsState()
// 检查是否隐藏红点
val isHideOtherInfo = settings.isHideOtherInfo
val showKpmInfo = settings.showKpmInfo
// 收集计数数据
val superuserCount by DataRefreshManager.superuserCount.collectAsState()
val moduleCount by DataRefreshManager.moduleCount.collectAsState()
val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState()
NavigationBar(
modifier = Modifier.windowInsetsPadding(
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
),
containerColor = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
).containerColor,
tonalElevation = cardElevation
) {
BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && !showKpmInfo && Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) {
if (!isFullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (!isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
BadgedBox(
badge = {
if (kpmModuleCount > 0 && !isHideOtherInfo) {
Badge(
containerColor = MaterialTheme.colorScheme.secondary
) {
Text(
text = kpmModuleCount.toString(),
style = MaterialTheme.typography.labelSmall
)
}
}
}
) {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
}
},
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
alwaysShowLabel = false
)
}
} else if (destination == BottomBarDestination.SuperUser) {
if (!isFullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
BadgedBox(
badge = {
if (superuserCount > 0 && !isHideOtherInfo) {
Badge(
containerColor = MaterialTheme.colorScheme.secondary
) {
Text(
text = superuserCount.toString(),
style = MaterialTheme.typography.labelSmall
)
}
}
}
) {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
}
},
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
alwaysShowLabel = false
)
} else if (destination == BottomBarDestination.Module) {
if (!isFullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
BadgedBox(
badge = {
if (moduleCount > 0 && !isHideOtherInfo) {
Badge(
containerColor = MaterialTheme.colorScheme.secondary)
{
Text(
text = moduleCount.toString(),
style = MaterialTheme.typography.labelSmall
)
}
}
}
) {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
}
},
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
alwaysShowLabel = false
)
} else {
if (!isFullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (isCurrentDestOnBackStack) {
navigator.popBackStack(destination.direction, false)
}
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = {
if (isCurrentDestOnBackStack) {
Icon(destination.iconSelected, stringResource(destination.label))
} else {
Icon(destination.iconNotSelected, stringResource(destination.label))
}
},
label = { Text(stringResource(destination.label),style = MaterialTheme.typography.labelMedium) },
alwaysShowLabel = false
)
}
}
}
}

View File

@@ -0,0 +1,24 @@
package zako.zako.zako.zakoui.activity.util
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.runtime.Composable
object AnimatedBottomBar {
@Composable
fun AnimatedBottomBarWrapper(
showBottomBar: Boolean,
content: @Composable () -> Unit
) {
AnimatedVisibility(
visible = showBottomBar,
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut()
) {
content()
}
}
}

View File

@@ -0,0 +1,109 @@
package zako.zako.zako.zakoui.activity.util
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ui.util.getKpmModuleCount
import com.sukisu.ultra.ui.util.getKpmVersion
import com.sukisu.ultra.ui.util.getModuleCount
import com.sukisu.ultra.ui.util.getSuperuserCount
import com.sukisu.ultra.ui.util.rootAvailable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
object AppData {
object DataRefreshManager {
// 私有状态流
private val _superuserCount = MutableStateFlow(0)
private val _moduleCount = MutableStateFlow(0)
private val _kpmModuleCount = MutableStateFlow(0)
// 公开的只读状态流
val superuserCount: StateFlow<Int> = _superuserCount.asStateFlow()
val moduleCount: StateFlow<Int> = _moduleCount.asStateFlow()
val kpmModuleCount: StateFlow<Int> = _kpmModuleCount.asStateFlow()
/**
* 刷新所有数据计数
*/
fun refreshData() {
_superuserCount.value = getSuperuserCountUse()
_moduleCount.value = getModuleCountUse()
_kpmModuleCount.value = getKpmModuleCountUse()
}
/**
* 异步刷新所有数据
*/
suspend fun refreshDataAsync() = withContext(Dispatchers.IO) {
refreshData()
}
}
/**
* 获取超级用户应用计数
*/
fun getSuperuserCountUse(): Int {
return try {
if (!rootAvailable()) return 0
getSuperuserCount()
} catch (_: Exception) {
0
}
}
/**
* 获取模块计数
*/
fun getModuleCountUse(): Int {
return try {
if (!rootAvailable()) return 0
getModuleCount()
} catch (_: Exception) {
0
}
}
/**
* 获取KPM模块计数
*/
fun getKpmModuleCountUse(): Int {
return try {
if (!rootAvailable()) return 0
val kpmVersion = getKpmVersionUse()
if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0
getKpmModuleCount()
} catch (_: Exception) {
0
}
}
/**
* 获取KPM版本
*/
fun getKpmVersionUse(): String {
return try {
if (!rootAvailable()) return ""
val version = getKpmVersion()
if (version.isEmpty()) "" else version
} catch (e: Exception) {
"Error: ${e.message}"
}
}
/**
* 检查是否具有管理员权限
*/
fun isManager(packageName: String): Boolean {
return Natives.becomeManager(packageName)
}
/**
* 检查是否是完整功能模式
*/
fun isFullFeatured(packageName: String): Boolean {
val isManager = Natives.becomeManager(packageName)
return isManager && !Natives.requireNewKernel() && rootAvailable()
}
}

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