612 Commits

Author SHA1 Message Date
Der_Googler
c5ed6e1e8c manager: Update WebUI X to the latest version (#345)
* manager: bump WebUI X

* manager: forgot to add interface name back

* manager: use compose set content for webuix

* manager: fix :jna library and reflections in prod
2025-08-21 07:21:43 +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
ShirkNeko
d743073309 docs: Add instructions for manually integrating susfs 2025-06-01 00:20:38 +08:00
ShirkNeko
a636911612 [skip ci]: New Crowdin updates (#123)
* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-06-01 00:16:11 +08:00
ShirkNeko
7a62f91752 manager: Using SwitchItem instead of ListItem 2025-06-01 00:07:17 +08:00
米凛MiRin
b551a54c8f manager: convert bitmap image to vector by hand. document: add copyright infomation. (#121) 2025-05-31 23:30:37 +08:00
ShirkNeko
26d86aa2fe manager: Optimize Home data refresh logic 2025-05-31 23:29:34 +08:00
ShirkNeko
6ee9246650 Fixes the problem of not refreshing automatically 2025-05-31 20:59:47 +08:00
ShirkNeko
1cd96fbdbf Optimize data preloading 2025-05-31 20:40:55 +08:00
ShirkNeko
a030a026b1 Manager: optimizing Home's data caching logic 2025-05-31 20:26:23 +08:00
ShirkNeko
8bf9cd0bee manager: Add initialization to optimize loading of SuperUser and Home data 2025-05-31 19:17:43 +08:00
ShirkNeko
13b1aad4b8 manager: Optimizing Home Performance
- Reorganize Home structure using MVVM architecture pattern to separate UI and data logic
2025-05-31 17:39:24 +08:00
ShirkNeko
916d956ce2 [skip ci]: New Crowdin updates (#120)
* Update source file strings.xml

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-05-31 13:28:54 +08:00
ShirkNeko
87a7650d26 New Crowdin updates (#119)
* Update source file strings.xml

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

* Update source file strings.xml
2025-05-31 13:19:14 +08:00
ShirkNeko
3484e187da manager: Refactoring the settings interface
- Merge Web Debugging and Web X Eruda switches
2025-05-31 12:49:58 +08:00
ShirkNeko
0835f330e2 manager: Modifying the WebUI Engine Global Options 2025-05-31 04:41:16 +08:00
ShirkNeko
8064472477 manager: better handle webui engine select
- Optimize the flashback problem caused by null pointer

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Der_Googler <54764558+DerGoogler@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-31 03:03:22 +08:00
ShirkNeko
2281012e33 manager: Fallback allows developers to override user preferences for the selected WebUI engine
- Because the WebUI can cause some problems, the automatic fetching of the
2025-05-31 02:14:25 +08:00
ShirkNeko
83eaeab1ba New Crowdin updates (#115)
* 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 (Russian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Chinese Traditional, Hong Kong)
2025-05-30 20:17:37 +08:00
ShirkNeko
6405764df3 Adjust settings and optimize theme styles 2025-05-30 20:00:33 +08:00
ShirkNeko
253276a27b Remove border styles from labels 2025-05-29 20:25:17 +08:00
ShirkNeko
855a71ac56 Adjust the Dark Mode Card Transparency setting to the default value of 1 2025-05-29 18:48:43 +08:00
ShirkNeko
96dc53977f manager: Refactoring kernel flash features and styles 2025-05-29 18:25:45 +08:00
ShirkNeko
31111e68eb [skip ci]: New translations strings.xml (Russian) (#113) 2025-05-29 15:21:14 +08:00
ShirkNeko
ac0de29872 Remove cards from Home 2025-05-29 15:00:05 +08:00
ShirkNeko
9e2b722491 manager: Adding Vacancies to WeiUI Configuration and Implementing Asynchronous Loading
- Upgrade agp version to 8.10.1
2025-05-29 14:44:24 +08:00
ShirkNeko
59627e6fe2 manager: update Crowdin workflow to include resource paths 2025-05-28 13:53:58 +08:00
ShirkNeko
cd0b5fb378 New Crowdin updates (#111)
* New translations strings.xml (Romanian)

* New translations strings.xml (French)

* New translations strings.xml (Spanish)

* New translations strings.xml (Arabic)

* New translations strings.xml (German)

* New translations strings.xml (Italian)

* 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 (Portuguese, Brazilian)

* New translations strings.xml (Persian)

* New translations strings.xml (Azerbaijani)

* New translations strings.xml (Hindi)

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

* New translations strings.xml (Kannada)

* New translations strings.xml (Spanish)

* New translations strings.xml (Danish)

* New translations strings.xml (Hungarian)

* New translations strings.xml (Korean)

* New translations strings.xml (Lithuanian)

* New translations strings.xml (Dutch)

* New translations strings.xml (Polish)

* New translations strings.xml (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* 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 (Malay)

* New translations strings.xml (Filipino)

* New translations strings.xml (Bosnian)
2025-05-28 13:47:41 +08:00
ShirkNeko
48a3c64c7c New translations strings.xml (Vietnamese) (#109) 2025-05-28 12:22:58 +08:00
米凛MiRin
62da804518 manager: ksuEngine as default WebUI engine (#110) 2025-05-28 12:22:38 +08:00
ShirkNeko
439b99cc4a manager: refactor label item in superuser list
* manager: Improvements

* manager: bump mmrl

* manager: use ktx ext Str.toUri

* manager: add "webui-engine" from config.json

This allows the developer to override the user preference of the selected WebUI engine.

Supported engines are:

- `wx` for WebUI X
- `ksu` for the KernelSU WebUI

All not named strings will default to `wx`

R.string.use_webuix_summary needs proper translations

* manager: add support for multilingual module meta

Co-authored-by: Der_Googler <54764558+DerGoogler@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-27 16:57:54 +08:00
ShirkNeko
64f0efc2c0 manager: use myUserId as fallback
Co-authored-by: Der_Googler <54764558+DerGoogler@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-27 16:33:58 +08:00
ShirkNeko
f196bf5b76 manager: Updated Kpm and version info icons 2025-05-27 16:02:36 +08:00
ShirkNeko
790968be6a manager: Change icon 2025-05-27 15:47:24 +08:00
ShirkNeko
83f0f9537f New Crowdin updates (#107)
* New translations strings.xml (French)

* New translations strings.xml (Arabic)

* New translations strings.xml (German)

* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Portuguese, Brazilian)

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

* New translations strings.xml (Thai)

* New translations strings.xml (Romanian)

* New translations strings.xml (Spanish)

* 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 (Slovenian)

* New translations strings.xml (Turkish)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Chinese Traditional)

* New translations strings.xml (Persian)

* New translations strings.xml (Marathi)

* 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)
2025-05-27 01:33:53 +08:00
ShirkNeko
68ebfec918 manager: Optimize the logic of displaying the Machine Architecture tab of the home status card
Fix the problem of displaying text in safe mode
2025-05-27 01:07:24 +08:00
ShirkNeko
8be4dea081 manager: Update interface card color to surfaceContainerLow to optimize visual effect. 2025-05-24 22:00:52 +08:00
Rifat Azad
cfdbba45c3 manager: make action execution screen have the same behavior as Magisk
based on pr https://github.com/tiann/KernelSU/pull/2321

* Magisk's behavior: Hide Bottom Navbar, Show close button if failed or success
and removed automatic exit when module execution success.
2025-05-24 17:03:20 +08:00
WenHao2130
d408c9f4bf manager: Modify Module page icon (#104)
Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-05-24 15:33:50 +08:00
ShirkNeko
8f4c58c4c3 [skip ci]: kernel: simplify KPM enabled check in ksu_handle_prctl 2025-05-24 15:25:54 +08:00
rsuntk
7e88e9648f kernel: guard nuke_ext4_sysfs
Rather than using depends on / select,
i just prefer this way, although, yes, it is
an ifdef hell.

Signed-off-by: rsuntk <rsuntk@yukiprjkt.my.id>
2025-05-24 15:05:44 +08:00
ShirkNeko
4516d136a4 Merge branch 'main' of https://github.com/SukiSU-Ultra/SukiSU-Ultra 2025-05-24 14:11:35 +08:00
ShirkNeko
1b85dfbed1 manager: Modify the text padding in the ElevatedCard
- Adding Formatting Characters

Signed-off-by: WenHao2130 <wenhao2130@outlook.com>
2025-05-24 14:11:14 +08:00
ShirkNeko
807ffb419a [skip ci] : Update source file strings.xml (#101) 2025-05-24 13:55:43 +08:00
ShirkNeko
e826f43aed Optimize KPM checking logic
- Simplify code and ensure KPM information is displayed under supported versions
2025-05-24 04:38:54 +08:00
ShirkNeko
d619f5fafc Refactoring KPM support to check KPM status using CMD_ENABLE_KPM 2025-05-24 03:28:28 +08:00
ShirkNeko
b3e2f9b7ff manager: Updated colors and styles
- Adapted from reference style

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: Der_Googler <54764558+DerGoogler@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-05-24 00:47:15 +08:00
ShirkNeko
99a39c6f52 New Crowdin updates (#100)
* Update source file strings.xml

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

* Update source file strings.xml
2025-05-23 17:06:24 +08:00
ShirkNeko
22991e8740 Merge branch 'main' of https://github.com/SukiSU-Ultra/SukiSU-Ultra 2025-05-23 16:46:31 +08:00
ShirkNeko
7646ecb6f7 manager: Update theme color scheme, fix style
* Remove redundant strings
* Bump MMRL

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: liaowenxuan <jby13147208050@163.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-23 16:45:17 +08:00
ShirkNeko
204db674bb [skip ci]: New Crowdin updates (#99)
* New translations strings.xml (Spanish)

* New translations strings.xml (Portuguese, Brazilian)
2025-05-23 16:38:20 +08:00
ShirkNeko
99fe6623de manager: possible fix
- Possible fix a bug where IUserManager.getUsers(ZZZ) are not defined in the framework.jar
- Refactored WebUI X to meet the new WXInterface
- Only fetch the app from the current user and not all users

* manager: remove unused AIDL interfaces

Signed-off-by: Der_Googler <54764558+DerGoogler@users.noreply.github.com>
2025-05-23 16:31:13 +08:00
ShirkNeko
f1f78d2485 Add force refresh to get module list after installing a module 2025-05-22 18:12:13 +08:00
ShirkNeko
b2ae20b796 manager: Enhance and simplify module name capture
- Add the use of incoming module name to load the corresponding installation list when the file cannot be retrieved, using utf-8 encoding and formatting characters by default.
2025-05-20 22:23:45 +08:00
ShirkNeko
83bd4e9642 New translations strings.xml (French) (#95) 2025-05-20 19:34:08 +08:00
ShirkNeko
767349798a docs: Added a link to submit translations to the Crowdin project page in the documentation 2025-05-20 19:31:44 +08:00
Jiu
ae38f4709b [skip ci]: 删掉本地配置sdk (#94)
* feat: Update string resources for clarity and consistency; simplify build manager workflow

* 使用gki-kernel-local.yml

* build-lkm-local.yml

* 修改为谷歌源

* 修改为清华源

* 修改max-size为16G

* Updating Vietnamese strings

* kernel: kpm: add compatibility for kernel 4.14 and lower (#76)

`thread_pid` is not defined in kernel 4.14 and lower, leading to compilation issue.
To fix this, use `pids[PIDTYPE_PID].pid` for kernel versions 4.14 and lower.
Else use `thread_pid` for kernel versions 4.19 and higher.

Reference: 107717913b/tracee/tracee.bpf.c (L354)

* 1

* 1

* 1

---------

Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Co-authored-by: KernelSUBot <bot@kernelsu.org>
Co-authored-by: sidex15 <24408329+sidex15@users.noreply.github.com>
Co-authored-by: build <123456@cnm.com>
2025-05-20 19:26:49 +08:00
ShirkNeko
fc7001a11a New Crowdin updates (#93)
* Update source file strings.xml

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

* Update source file strings.xml

* New translations strings.xml (Japanese)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Vietnamese)
2025-05-20 13:22:44 +08:00
ShirkNeko
9924809bdb [skip ci]: Update the GitHub repository links in the documentation to ensure that they point to the correct SukiSU-Ultra repositories 2025-05-20 12:48:06 +08:00
ShirkNeko
58a4ff94e4 Add module download error alerts and optimize update checking logic
- Add a formatting string for the update list
- Fix module update failures caused by spaces and other non Linux readable characters.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-20 12:12:22 +08:00
ShirkNeko
29033e9b80 [skip ci]: New Crowdin updates (#90)
* New translations strings.xml (French)

* 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 (Romanian)

* 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 (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 (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)
2025-05-19 23:44:53 +08:00
ShirkNeko
ea24daf37c Update Crowdin configuration file 2025-05-19 21:32:29 +08:00
ShirkNeko
ebc16583fb [skip ci]:kernel: kpm: add compatibility for kernel 4.14 and lower (#76)
manger: Fix and simplify back gesture

`thread_pid` is not defined in kernel 4.14 and lower, leading to compilation issue.
To fix this, use `pids[PIDTYPE_PID].pid` for kernel versions 4.14 and lower.
Else use `thread_pid` for kernel versions 4.19 and higher.

Reference: 107717913b/tracee/tracee.bpf.c (L354)

Co-authored-by: sidex15 <24408329+sidex15@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-05-19 21:30:13 +08:00
ShirkNeko
2a10b41781 [skip ci]: Adding Flash related string resources 2025-05-19 17:17:42 +08:00
ShirkNeko
d5946047a1 manger: update flash style
add instructions to make it easier to understand
2025-05-19 16:49:37 +08:00
ShirkNeko
4ff46a4911 manager: Enhance Flash module handling
- Module screen for batch installation

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-19 14:07:40 +08:00
ShirkNeko
b587216b5e Updating Vietnamese strings 2025-05-18 21:21:21 +08:00
ShirkNeko
245fce167e manager: Optimize device model and KPM configuration checking
Add caching mechanism to improve performance
2025-05-18 20:16:04 +08:00
ShirkNeko
de9b82ffd5 [skip ci]: feat: Update string resources for clarity and consistency; simplify build manager workflow 2025-05-18 19:35:05 +08:00
ShirkNeko
e570f402e4 feat: Add a GitHub workflow for building LKM locally 2025-05-18 17:01:28 +08:00
ShirkNeko
9c761b13fa feat: Adding a GitHub workflow with a manual build manager 2025-05-18 16:43:17 +08:00
cvnertnc
cc4b135d20 Manager: update values-tr/strings.xml Docs: added README-tr.md (#83)
Manager: update values-tr/strings.xml
Docs: added README-tr.md
2025-05-18 11:38:51 +08:00
ShirkNeko
ec5395c787 Remove unnecessary patches 2025-05-18 04:07:53 +08:00
ShirkNeko
6d60e54a7d feat: Enhance KPM configuration checking,
- remove unused imports, update mmrl versions
2025-05-18 04:06:36 +08:00
ShirkNeko
28aa34c0b6 Updating the KPM configuration
- We don't know if KPM can run on arm32-bit devices, so to avoid some problems, add a dependency on 64-bit architectures

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-17 22:22:13 +08:00
ShirkNeko
0701967bab [skip ci]: ci: update kmi versions 2025-05-17 21:15:07 +08:00
Wang Han
a76b1eece4 Fix fallback option for createRootShell() (#2593) 2025-05-17 21:10:52 +08:00
ShirkNeko
8e791c680e docs: 添加爱发电链接至 README.md 2025-05-17 17:54:22 +08:00
ShirkNeko
fc9f2ccf25 Add Icon Patch 2025-05-17 17:48:55 +08:00
ShirkNeko
d4682fb06e manager: Update secondary interface status and optimize WebView interface 2025-05-17 17:09:47 +08:00
ShirkNeko
377ea183a7 Updated Vietnamese Translation 2025-05-16 22:01:04 +08:00
ShirkNeko
72361ab8bf manager: Modify the batch selection ui on the superuser page
- Add more convenient buttons for it
2025-05-16 16:00:51 +08:00
ShirkNeko
f708e583c3 docs: updated to reflect changes to support for non-GKI devices.
- Adjusted branch usage instructions and KPM support information
2025-05-15 22:55:18 +08:00
ShirkNeko
d753e1dc48 [ship ci]: Updated Vietnamese Translation 2025-05-15 22:06:49 +08:00
ShirkNeko
315a8a3805 Normalize kernel related constants to restore 2025-05-15 20:59:44 +08:00
ShirkNeko
129fed9c9f manager: simplify kernel arch
Previously:

Kernel
4.19.331-Rissu

Kernel Arch
armv8l

This changes:

Kernel
4.19.331-Rissu (armv8l)

Suggested-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
Signed-off-by: rsuntk <90097027+rsuntk@users.noreply.github.com>
2025-05-15 20:40:50 +08:00
ShirkNeko
0baccb7621 Add ksud support for the armeabi-v7a architecture
Co-authored-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
Co-authored-by: SChernykh <15806605+SChernykh@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-05-15 20:00:51 +08:00
backslashxx
842a8aa45a kernel/selinux: fix pointer mismatch with 32-bit ksud on 64-bit kernels
Since KernelSU Manager can now be built for 32-bit, theres this problematic
setup where userspace is 32-bit (armeabi-v7a) and kernel is 64bit (aarch64).

On 64-bit kernels with CONFIG_COMPAT=y, 32-bit userspace passes 32-bit pointers.
These values are interpreted as 64-bit pointers without proper casting and that
results in invalid or near-null memory access.

This patch adds proper compat-mode handling with the ff changes:
- introduce a dedicated struct (`sepol_compat_data`) using u32 fields
- use `compat_ptr()` to safely convert 32-bit user pointers to kernel pointers
- adding a runtime `ksu_is_compat` flag to dynamically select between struct layouts

This prevents a near-null pointer dereference when handling SELinux
policy updates from 32-bit ksud in a 64-bit kernel.

Truth table:

kernel 32 + ksud 32, struct is u32, no compat_ptr
kernel 64 + ksud 32, struct is u32, yes compat_ptr
kernel 64 + ksud 64, struct is u64, no compat_ptr

Preprocessor check

64BIT=y COMPAT=y: define both structs, select dynamically
64BIT=y COMPAT=n: struct u64
64BIT=n: struct u32

Tested-by: ...
Tested-by: ...
Tested-by: ...
Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
2025-05-15 17:39:41 +08:00
backslashxx
d17843479c kernel/throne_tracker: we just uninstalled the manager, stop looking for it
When the manager UID disappears from packages.list, we correctly
invalidate it — good. But, in the very next breath, we start scanning
/data/app hoping to find it again?

This event is just unnecessary I/O, exactly when we should be doing less.
Apparently this causes hangups and stuckups which is REALLY noticeable
on Ultra-Legacy devices.

Skip the scan — we’ll catch the reinstall next time packages.list updates.

Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
2025-05-15 17:39:41 +08:00
backslashxx
0d70cc8e58 kernel: sucompat: sucompat toggle support for non-kp (tiann#2506)
This is done like how vfs_read_hook, input_hook and execve_hook is disabled.
While this is not exactly the same thing, this CAN achieve the same results.
The complete disabling of all KernelSU hooks.

While this is likely unneeded, It keeps feature parity to non-kprobe builds.

adapted from upstream:
	kernel: Allow to re-enable sucompat - 4593ae81c7

Rejected: https://github.com/tiann/KernelSU/pull/2506

Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
2025-05-15 17:39:41 +08:00
Re*Index. (ot_inc)
4e6cacb206 [skip ci]: Update Japanese. (#74)
* Update strings.xml

* fix typo & change Japanese text.
2025-05-15 16:41:38 +08:00
ShirkNeko
52514ba35b [skip ci]: Move the language selection into the card 2025-05-15 16:40:27 +08:00
ShirkNeko
4d59ce435e Add card darkness adjustment function
- Updated some string translations
2025-05-14 19:55:11 +08:00
ShirkNeko
b3b7fa6f4d [skip ci]: Update language options
- Add Vietnamese support (from bro in the group)
2025-05-14 18:33:31 +08:00
ShirkNeko
c057c16391 Stand alone theme configuration for webuiX
- Add secondary color interface: isSecondaryPage (bool)
2025-05-14 16:29:27 +08:00
ShirkNeko
dee7cc6f2b Add language options
- Fix some icon color issues
2025-05-14 15:01:59 +08:00
ShirkNeko
3d0d87cb0c Add application DPI setting function
- Allow users to customize the display density of the current application

- Fix some popup color errors
2025-05-13 23:56:18 +08:00
ShirkNeko
6b66d9b3f8 Remove redundant definitions of KPM strings
Clean up unused code in build scripts
2025-05-13 21:57:42 +08:00
ShirkNeko
a301d94858 [skip ci]: Fix missing brackets in KPM feature information summary 2025-05-13 21:51:21 +08:00
ShirkNeko
01199470f2 [manager]: Add KPM function display options and related settings
- Eruda injection web UI X will not be displayed when the modification is not enabled.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-05-13 21:44:42 +08:00
ShirkNeko
9e7ea19567 [skip ci]:Update some descriptions 2025-05-13 18:30:39 +08:00
Der_Googler
cdc6a6cb4a Add option to use WebUI X
- Added WebUI X from MMRL

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

* Update README-en.md

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

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

* manager: modify padding

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

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

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

---------

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

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

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

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

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

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

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

* Fix English translations

* Remove unused string entry

---------

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

* fix typo

* Update README-en.md

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

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

Co-authored-by: rsuntk <rsuntk@yukiprjkt.my.id>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-20 22:30:23 +08:00
ShirkNeko
1dd8651a1a Remove unused functions and data classes and optimize code structure; update string resources to support new features 2025-04-20 22:01:35 +08:00
ShirkNeko
33dd0ca16b Add check for GKI version and KERNEL_TYPE setting 2025-04-19 21:44:41 +08:00
Re*Index. (ot_inc)
dfe7852c25 fix typo (#50)
* Update Japanese!

* Fix typo

* Fix typo 2

* Update strings.xml

* Update strings.xml

* Update Japanese.

* Update strings.xml

* Add Japanese README

* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update README-en.md

* Update README-ja.md

* Update README-en.md

* Update README.md

* Update README-ja.md

* fix typo

* Update strings.xml
2025-04-19 16:03:25 +08:00
Re*Index. (ot_inc)
abe6184f63 Update READMEs. (#49)
* Update Japanese!

* Fix typo

* Fix typo 2

* Update strings.xml

* Update strings.xml

* Update Japanese.

* Update strings.xml

* Add Japanese README

* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update README-ja.md

* Update README-en.md

* Update README-ja.md

* Update README-en.md

* Update README.md

* Update README-ja.md
2025-04-18 20:16:31 +08:00
Re*Index. (ot_inc)
e8afb2143b Japanese Translate (#48)
* Update Japanese!

* Fix typo

* Fix typo 2

* Update strings.xml

* Update strings.xml

* Update Japanese.

* Update strings.xml

* Add Japanese README

* Update README-ja.md

* Update README-ja.md
2025-04-17 17:23:26 +08:00
ShirkNeko
7e1c363bad Add module uninstallation confirmation dialog and file type validation message 2025-04-16 14:36:27 +08:00
Kousei
bcf6809deb Add files via upload (#47) 2025-04-16 13:21:43 +08:00
ShirkNeko
bce5f6cf61 Fix GitHub link in About card 2025-04-15 15:09:36 +08:00
ShirkNeko
5495eebb38 Update version number 2025-04-14 20:18:52 +08:00
ShirkNeko
d255801666 Update version number 2025-04-14 20:18:24 +08:00
ShirkNeko
44f2601126 Update version number 2025-04-14 20:18:10 +08:00
ShirkNeko
3a7366c4bc Update version number 2025-04-14 20:18:04 +08:00
ShirkNeko
5da289714d Update version number 2025-04-14 20:17:51 +08:00
ShirkNeko
a18d718744 Update version number 2025-04-14 20:17:44 +08:00
ShirkNeko
c90fc461d9 Update version number 2025-04-14 20:17:37 +08:00
ShirkNeko
e80fbe8934 Update version number 2025-04-14 20:17:32 +08:00
ShirkNeko
e0650ade4f Update version number 2025-04-14 20:17:16 +08:00
ShirkNeko
378b8458f2 Opt device disabling logic in HomeScreen 2025-04-14 19:30:30 +08:00
ShirkNeko
182028d9ea Updating the downloader to use the new GitHub publishing interface 2025-04-14 19:06:54 +08:00
ShirkNeko
0c3b8e7610 1 2025-04-14 18:46:51 +08:00
ShirkNeko
332fdcd2a7 Fix KPM module loading calls to explicitly use namespaces 2025-04-14 15:55:35 +08:00
ShirkNeko
aa20d04d3a Refactoring KPM module loading logic, removing existing KPM loading functions, simplifying code and enhancing error handling 2025-04-14 15:51:47 +08:00
ShirkNeko
949106bc09 Refactoring KPM module loading logic, adding modification event handling, improving error handling 2025-04-14 15:39:43 +08:00
ShirkNeko
67babc2858 Enhanced KPM module loading logic, added filename validation and error handling 2025-04-14 15:35:11 +08:00
ShirkNeko
d087ec510e Ksud: refactor KPM event handling logic, add error handling and ensure KPM catalog exists 2025-04-14 15:26:30 +08:00
gggggdw
c2ed3da87c Update Engish translation (#46) 2025-04-14 14:39:04 +08:00
ShirkNeko
f2d159f732 Updated README file to add KPM support information and remove duplicate feature descriptions 2025-04-14 14:38:27 +08:00
ShirkNeko
468fc2207d Fixed integration note on susfs in README.md 2025-04-14 02:05:34 +08:00
Qumolama.d
eced8bae82 Fix typos and translations in README.md and README-en.md (#23)
* Fix typos in README.md

* Fix various translation error in README-en.md

* Update links in README-en.md
2025-04-14 02:04:35 +08:00
Wang Han
e61ecb3963 Fix opuls -> oplus typo (#2536) 2025-04-13 18:14:27 +08:00
Kousei
304f4f8b2c Update Vietnamese language translation (#44)
* Add files via upload

* Add files via upload
2025-04-13 16:00:30 +08:00
ShirkNeko
48888087e1 Fix branch checking in build-manager.yml by changing 'susfs' to 'main' 2025-04-13 15:41:33 +08:00
crazymrli
75a70e70be Merge pull request #43 from ShirkNeko/dev
Dev
2025-04-13 15:25:59 +08:00
liankong
cf825f912c 添加KPM模板说明 2025-04-13 15:22:22 +08:00
liankong
1944a49fd8 添加super_access对task_struct的支持 2025-04-13 14:41:06 +08:00
liankong
657f343f5c 添加super_access对task_struct的支持 2025-04-13 14:18:33 +08:00
ShirkNeko
74bb90b3d8 Update build-lkm.yml to force uploading inputs to be required, default value changed to true 2025-04-13 02:26:26 +08:00
Kousei
8bf9828c80 Add files via upload (#42) 2025-04-13 01:43:55 +08:00
ShirkNeko
06324def38 更新super_access.c to remove redundant macro definitions and optimize conditional compilation; use resource strings instead of hard-coded text in MoreSettings.kt; add theme color string resource in strings.xml 2025-04-13 01:33:40 +08:00
ShirkNeko
70259a5ec5 清理super_access.c中的多余空行 2025-04-12 17:00:14 +08:00
ShirkNeko
a63057c594 Merge branch 'dev' into main 2025-04-12 16:55:20 +08:00
ShirkNeko
48d5270611 Add super_access function to support dynamic access to structures and members 2025-04-12 16:52:13 +08:00
ShirkNeko
712d0f3342 更新super_access.c以支持不同内核版本的结构体成员定义 2025-04-12 16:29:13 +08:00
ShirkNeko
d6084aeca1 在super_access.c中添加对linux/version.h的引用,并定义KERNEL_VERSION_6_6宏,以支持不同内核版本的条件编译 2025-04-12 16:09:13 +08:00
ShirkNeko
562b9624d7 移除对linux/nsproxy.h的引用,并添加对../fs/mount.h的引用 2025-04-12 15:49:00 +08:00
ShirkNeko
a68d5e8bbe 在super_access.c中添加对linux/nsproxy.h的引用 2025-04-12 15:02:22 +08:00
ShirkNeko
d45aa8197e Remove unnecessary header file fs/mount.h and use linux/mount.h instead. 2025-04-12 14:54:10 +08:00
ShirkNeko
314d3ef97a 更新Makefile以使用super_access.o替代super_access.c 2025-04-12 14:44:50 +08:00
liankong
e3750ccd51 完善super_access 2025-04-12 10:35:08 +08:00
liankong
a712efe9d8 添加super_access机制 2025-04-12 00:34:37 +08:00
ShirkNeko
2266362e24 Changing the package name 2025-04-12 00:33:46 +08:00
ShirkNeko
b7056b5baa Changing the package name 2025-04-12 00:33:29 +08:00
liankong
8bd07bf56c Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-04-11 15:19:48 +08:00
liankong
569183efe9 添加super_access机制 2025-04-11 15:19:18 +08:00
crazymrli
937cf25e9b Merge pull request #38 from ShirkNeko/main
Merge to dev
2025-04-11 15:17:01 +08:00
Kousei
e2b6617577 Add String-vi (#36)
Add Vietnamese language translation
2025-04-10 23:42:28 +08:00
Re*Index. (ot_inc)
8323d6394b Fix Japanese Translate! (#35)
* Update Japanese!

* Fix typo

* Fix typo 2

* Update strings.xml

* Update strings.xml
2025-04-10 23:39:02 +08:00
Re*Index. (ot_inc)
e31b892a20 Update Japanese Translate! (#34)
* Update Japanese!

* Fix typo
2025-04-10 15:48:24 +08:00
ShirkNeko
65b1518e26 Opt KPM module installation to support URL-encoded filenames and extract module IDs for dynamically constructed names.
This will fix the flashback issue due to path issue

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-10 01:25:59 +08:00
ShirkNeko
865dbd3799 feat: Load Existing KPM Modules on KPM Monitor Startup 2025-04-08 19:03:29 +08:00
ShirkNeko
7de9d7967a Add embedded and load-optimized KPM module installation and uninstallation prompt messages 2025-04-08 18:56:14 +08:00
ShirkNeko
aa2d2454e1 feat: 引入anyhow库以增强错误处理能力 2025-04-07 22:33:45 +08:00
ShirkNeko
b850336872 feat: Optimize KPM file processing logic, use is_some_and method to simplify extension checking, and enhance readability and stability of event processing. 2025-04-07 22:28:22 +08:00
ShirkNeko
138dec35c7 feat: Add KPM module management function, support loading, unloading and deleting all KPM modules. 2025-04-07 22:13:21 +08:00
ShirkNeko
569fffa962 feat: 引入notify库以支持KPM目录的文件监控 2025-04-07 21:40:25 +08:00
ShirkNeko
2a283e6793 feat: 修复unload_kpm函数的定义,添加缺失的fn关键字 2025-04-07 21:35:44 +08:00
ShirkNeko
73a7ba3ac9 feat: 将KPM相关函数公开以支持外部调用 2025-04-07 21:32:41 +08:00
ShirkNeko
c0c4ea9f86 feat: 引入KPM模块以支持文件系统数据处理 2025-04-07 21:26:08 +08:00
ShirkNeko
a13179cd09 feat: 添加KPM模块以支持KPM相关功能 2025-04-07 21:21:36 +08:00
ShirkNeko
de089b7b73 feat: Adding a KPM monitor to handle KPM file creation and deletion events 2025-04-07 21:18:14 +08:00
ShirkNeko
1b700fb8e0 Merge pull request #33 from ShirkNeko/dev
增加了两个必要的兼容函数,以确保能够运行Peekaboo.KPM
2025-04-07 19:48:03 +08:00
liankong
f0febf13f2 增加了两个必要的兼容函数,以确保能够运行Peekaboo.KPM 2025-04-07 19:43:06 +08:00
ShirkNeko
a002967a92 Refactor dialog implementation, replace ListDialog with AlertDialog to optimize user interaction experience.
This will improve fixing KMI and restoring init_boot image style issues
2025-04-07 16:56:40 +08:00
ShirkNeko
1167b20d89 Merge pull request #32 from ShirkNeko/dev 2025-04-07 15:40:48 +08:00
ShirkNeko
297ff3ae90 Dev (#31)
* 添加安装工具类,并增加把kpmmgr安装到/data/adb/ksu/bin的功能,方便模块调用

* Update README

* Update README

---------

Co-authored-by: liankong <xhsw.new@qq.com>
2025-04-07 15:38:04 +08:00
liankong
a39e2ce15a Update README 2025-04-07 15:36:46 +08:00
liankong
78eda275d6 Update README 2025-04-07 15:35:21 +08:00
ShirkNeko
1fce0fd77d 添加安装工具类,并增加把kpmmgr安装到/data/adb/ksu/bin的功能,方便模块调用 (#30)
Co-authored-by: liankong <xhsw.new@qq.com>
2025-04-07 15:17:46 +08:00
liankong
c942393f21 添加安装工具类,并增加把kpmmgr安装到/data/adb/ksu/bin的功能,方便模块调用 2025-04-07 15:09:05 +08:00
ShirkNeko
040cc30e73 manager: Updates the sub-levels and patch levels of the Android version 2025-04-06 13:54:57 +08:00
ShirkNeko
6b75ffc928 Delete .github/dependabot.yml 2025-04-06 00:24:46 +08:00
WenHao2130
a7f1b21f91 manager: move more_settings_simplicity_mode to custom_settings (#28)
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-05 15:28:09 +08:00
WenHao2130
c8f7d9d5bc manager: beta -> Beta (#27)
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-05 11:46:41 +08:00
WenHao2130
2520d45dc4 manager: add some animation (#26)
Signed-off-by: WenHao2130 <98936399+WenHao2130@users.noreply.github.com>
2025-04-05 02:59:25 +08:00
WenHao2130
8bdf8d98c3 manager: allow hide susfs status information
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-04 21:32:06 +08:00
WenHao2130
04025f3d32 manager: allow hide some other information
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-04 21:32:06 +08:00
ShirkNeko
4c7ed9c8ee 修正 SukiSU Ultra 展望字符串中的空格格式 2025-04-04 18:01:57 +08:00
ShirkNeko
c36f8b0df3 Opt the transparency slider display logic with AnimatedVisibility and update the formatting in the Chinese string resource.
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-04-04 17:51:27 +08:00
WenHao2130
5ccec93940 manager: Use AnimatedVisibility for Exclude Modifications SwitchItem
Co-authored-by: Light summer <93428659+lightsummer233@users.noreply.github.com>
Co-authored-by: WenHao2130 <WenHao2130@outlook.com>
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-04 17:24:36 +08:00
ShirkNeko
14b15e18f3 Update README.md 2025-04-04 15:49:21 +08:00
ShirkNeko
3e6f3b4d80 Merge pull request #22 from WenHao2130/main
ksud: update banner
2025-04-04 15:04:48 +08:00
WenHao2130
470b3106cb ksud: update banner
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-04 15:00:55 +08:00
ShirkNeko
e0074bc3ab Docs: simplify susfs branch usage instructions, remove redundant information 2025-04-04 14:27:07 +08:00
ShirkNeko
8d04ecdc52 Adjust card transparency and shadow settings to improve visualization 2025-04-04 13:44:21 +08:00
ShirkNeko
c83b1e88b9 Merge pull request #19 from ShirkNeko/dev
Dev
2025-04-03 23:34:01 +08:00
ShirkNeko
0446cc499e Merge branch 'main' into dev 2025-04-03 23:33:04 +08:00
ShirkNeko
8a6507e834 Manager: Add clean mode switch and option to hide kernel version number 2025-04-03 23:23:05 +08:00
ShirkNeko
09893b9472 Merge pull request #18 from WenHao2130/main
manager: optimize layout
2025-04-03 16:07:38 +08:00
WenHao2130
13d2290205 manager: optimize layout
Signed-off-by: WenHao2130 <WenHao2130@outlook.com>
2025-04-03 15:59:00 +08:00
ShirkNeko
46e4c85563 Add missing package names 2025-04-02 22:58:13 +08:00
ShirkNeko
d925036bd6 Optimize the error handling logic for loading and unloading modules, this will fix the file type error, and more detail handling-2 2025-04-02 17:59:59 +08:00
ShirkNeko
fbf2799674 Opt the error handling logic for loading and unloading modules, this will fix the file type error, and more detail handling 2025-04-02 17:59:29 +08:00
liankong
12c7558b91 给WebUI添加对KPM的操控支持 2025-04-02 14:03:50 +08:00
ShirkNeko
d55af76260 Optimize the error handling logic for loading and unloading modules, this will fix the file type error, and more detail handling-2 2025-04-02 00:36:08 +08:00
ShirkNeko
e4c70e2efb Opt the error handling logic for loading and unloading modules, this will fix the file type error, and more detail handling 2025-04-02 00:35:55 +08:00
ShirkNeko
a971fee132 Revert "Add KPM module count display to the main page and update related string resources"
This reverts commit a45e0f78ef.
2025-04-01 17:27:51 +08:00
ShirkNeko
a45e0f78ef Add KPM module count display to the main page and update related string resources 2025-04-01 17:24:13 +08:00
ShirkNeko
bf07dcb9ea Merge pull request #14 from ShirkNeko/dev
Add input dialogs to the KPM module to optimize control logic and dis…
2025-04-01 17:05:09 +08:00
ShirkNeko
e5f5b8f831 Add input dialogs to the KPM module to optimize control logic and display Snackbar prompts for operation results. 2025-04-01 17:04:00 +08:00
ShirkNeko
a80513fc50 Merge pull request #13 from ShirkNeko/dev
Dev
2025-04-01 16:18:17 +08:00
ShirkNeko
f71de1742a 优化KPM模块列表的获取逻辑,添加定时刷新功能,并调整空状态显示 2025-04-01 16:16:56 +08:00
ShirkNeko
57c65fdcda 移除调试签名配置,优化KPM版本显示逻辑,添加KPM控制成功和失败的提示字符串 2025-04-01 16:09:18 +08:00
ShirkNeko
0a9cf5f9aa Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-04-01 15:56:40 +08:00
ShirkNeko
08fdf2bdad 为KPM模块添加控制功能,显示操作成功或失败的Snackbar提示,并优化模块信息获取逻辑 2025-04-01 15:56:30 +08:00
liankong
690c6bac38 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-04-01 15:54:03 +08:00
liankong
af81308097 修复获取不到完整信息的bug 2025-04-01 15:53:58 +08:00
ShirkNeko
90b79f5c04 将KPM模块的Snackbar持续时间从长改为短,以改善用户体验 2025-04-01 15:04:09 +08:00
liankong
0c5dcec7bc 尝试修复KPM信息解析 2025-04-01 14:57:22 +08:00
ShirkNeko
a30dfbc15d 优化KPM模块加载和卸载逻辑,改进错误处理,增强模块信息获取功能 2025-04-01 14:34:24 +08:00
ShirkNeko
52f3335977 Merge pull request #12 from ShirkNeko/dev
Dev
2025-04-01 13:56:08 +08:00
liankong
c8b3e953ad Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-04-01 12:29:06 +08:00
liankong
e9d0526e1b 1 2025-04-01 12:28:59 +08:00
ShirkNeko
408c3be675 优化Kpm模块相关函数的返回类型,改为Boolean或Int以提高可读性和准确性 2025-04-01 02:40:58 +08:00
ShirkNeko
f67f16733f 优化KPM模块加载逻辑,更新相关字符串资源,调整KPM版本显示条件 2025-03-31 23:22:03 +08:00
ShirkNeko
25a173ad7b Merge pull request #8 from ShirkNeko/dev
Dev
2025-03-31 22:34:09 +08:00
ShirkNeko
75ec88f7a7 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 22:33:23 +08:00
ShirkNeko
e9c5ffb430 优化HomeScreen,移除KPM状态检查,调整ContributionCard显示逻辑 2025-03-31 22:33:20 +08:00
ShirkNeko
cb2cdaed12 删除KernelConfigUtils.kt文件 2025-03-31 22:32:57 +08:00
liankong
029c7f1e2a Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 22:32:48 +08:00
liankong
5a8d6895fa 添加加载成功提示 2025-03-31 22:32:42 +08:00
ShirkNeko
bb11e23006 Merge pull request #7 from ShirkNeko/dev
Dev
2025-03-31 22:12:28 +08:00
ShirkNeko
37b00d49c8 添加KPM状态检查功能,并更新相关UI文本 2025-03-31 22:11:52 +08:00
liankong
8055aed507 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 20:28:31 +08:00
liankong
e7cef05c6a 修复 2025-03-31 20:28:26 +08:00
ShirkNeko
313746b578 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 20:27:40 +08:00
ShirkNeko
a7c557222c 修复Makefile中KPM状态信息的重复输出 2025-03-31 20:27:28 +08:00
liankong
9c902fb264 2 2025-03-31 20:24:56 +08:00
liankong
079f74d960 更改返回值处理方式 2025-03-31 19:47:22 +08:00
liankong
00d7de5276 1 2025-03-31 19:07:07 +08:00
liankong
3e928365de 1 2025-03-31 18:51:20 +08:00
liankong
47ba174fb1 尝试支持clang关闭优化 2025-03-31 18:31:21 +08:00
liankong
9446296daa 尝试禁用对壳函数的优化 2025-03-31 18:19:55 +08:00
liankong
7175a6fa7d Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 18:08:20 +08:00
liankong
1b06f7d317 关掉所有对壳函数的优化 2025-03-31 18:08:09 +08:00
ShirkNeko
c739bf6bfb 更新应用名称为 SukiSU Ultra,并修改相关提示信息为 SukiSU beta版管理器 2025-03-31 17:30:51 +08:00
liankong
dec9a72b41 增加壳函数信息打印 2025-03-31 17:17:21 +08:00
liankong
593cbaa067 添加壳函数防止被inline优化掉 2025-03-31 16:55:51 +08:00
liankong
fb8906e371 4 2025-03-31 16:19:52 +08:00
liankong
df943250ac 3 2025-03-31 16:07:29 +08:00
liankong
5a522a1489 kpmmgr 2025-03-31 16:01:22 +08:00
liankong
0b0d64b9d0 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 16:00:21 +08:00
liankong
18876e8a69 修复kpmmgr 2025-03-31 16:00:09 +08:00
ShirkNeko
b668378e23 重命名函数并更新命令以获取KPM版本信息 2025-03-31 15:52:57 +08:00
liankong
9de2c09a27 更新kpmmgr 2025-03-31 15:40:28 +08:00
liankong
6b3d2bef12 1 2025-03-31 15:22:30 +08:00
liankong
344ed41bc7 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-31 15:13:24 +08:00
liankong
2e711c3ac9 3 2025-03-31 15:13:13 +08:00
ShirkNeko
1bf4486cf1 Merge pull request #6 from ShirkNeko/dev
Dev
2025-03-31 14:53:34 +08:00
ShirkNeko
cb116286ed Manager: Fix the problem that the contrast of key color is too light 2025-03-31 14:50:27 +08:00
ShirkNeko
7f0ae95dfb Improve font color blending and contrast in dark mode.
Fix login issue (#19).

Modify styles for more settings.

Do not show the status of susfs when using lkm mode.

Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-03-31 14:50:20 +08:00
ShirkNeko
fe7ec9dcf5 docs: update README to clarify branch usage and support for non-GKI devices 2025-03-31 14:50:07 +08:00
ShirkNeko
d88eccdda3 docs: add acknowledgment for yspbwx2010's support 2025-03-31 14:49:53 +08:00
liankong
78fe01d9a4 2 2025-03-31 14:30:43 +08:00
liankong
60cb41c76b 1 2025-03-31 14:04:04 +08:00
liankong
af78f3bac4 转变为仅在内核留下Stub在外部加载KPM 2025-03-31 13:55:48 +08:00
liankong
fff86dcc8d 4 2025-03-30 21:06:56 +08:00
liankong
5ec053ca34 3 2025-03-30 20:39:23 +08:00
liankong
e9f1631b06 根据KP修复 2025-03-30 20:29:43 +08:00
ShirkNeko
3705993330 Merge pull request #5 from ShirkNeko/dev
Dev
2025-03-30 20:21:37 +08:00
liankong
3db338da3e 2 2025-03-30 20:10:50 +08:00
liankong
d126d0f5b8 1 2025-03-30 20:03:40 +08:00
liankong
549adebb30 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-30 19:56:18 +08:00
liankong
40bada35c6 尝试修复 2025-03-30 19:56:04 +08:00
ShirkNeko
074903a299 更新支持的非官方管理器信息,添加 udochina 2025-03-30 19:50:14 +08:00
ShirkNeko
328bee94e5 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-30 19:45:12 +08:00
ShirkNeko
0db25f14f1 添加 KPM 状态信息输出,并更新支持的非官方管理器信息 2025-03-30 19:45:10 +08:00
liankong
877e4f9416 修复调用错误 2025-03-30 19:42:09 +08:00
liankong
8b3e864ffa Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-30 19:38:50 +08:00
liankong
5f5f677b7b 修复 2025-03-30 19:38:19 +08:00
ShirkNeko
3933d83d3e 更新 KPM 配置选项名称为 SukiSU,并修改帮助信息以反映新功能和潜在影响 2025-03-30 19:37:14 +08:00
liankong
4abd35fb44 10 2025-03-30 19:27:27 +08:00
liankong
e68afb04eb 9 2025-03-30 19:18:38 +08:00
liankong
bf2be96b29 8 2025-03-30 19:17:42 +08:00
liankong
b0b5048b01 7 2025-03-30 18:57:51 +08:00
liankong
aff69af690 6 2025-03-30 18:56:28 +08:00
liankong
c1d156cd6b 5 2025-03-30 18:50:33 +08:00
liankong
e58e00be9d Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-30 18:36:01 +08:00
liankong
440fe972f4 4 2025-03-30 18:35:47 +08:00
ShirkNeko
e24588b961 Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-30 18:31:09 +08:00
ShirkNeko
57c8d69e83 Manager: simplify and fix SUSFS version display errors
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-03-30 18:30:53 +08:00
liankong
79c0bebcf5 3 2025-03-30 18:30:44 +08:00
ShirkNeko
321c9c20d5 Manager: simplify and fix SUSFS version display errors
Signed-off-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
2025-03-30 18:30:04 +08:00
liankong
f6134b47da 2 2025-03-30 18:25:40 +08:00
liankong
8c282b28a0 1 2025-03-30 18:24:56 +08:00
liankong
656cd11876 根据KP修复 2025-03-30 18:10:14 +08:00
liankong
5e77c08872 尝试修复: 内存段缓存/权限问题? 2025-03-30 17:56:53 +08:00
liankong
1090f64117 修复符号引用问题 2025-03-30 17:32:35 +08:00
liankong
470aaa29dc 兼容修复 2025-03-30 17:21:33 +08:00
liankong
c6664af45b 修复 2025-03-30 17:15:25 +08:00
liankong
d6b0ce2565 修复堆栈支持 2025-03-30 17:09:51 +08:00
liankong
770c9632ae 添加panic时打印出对应KPM信息的情况 2025-03-30 16:52:44 +08:00
liankong
cd60773d73 修复编译规则 2025-03-30 16:20:35 +08:00
liankong
06cdd92129 BypassCFI 2025-03-30 16:19:36 +08:00
liankong
315df33bd6 添加跳过CFI检查的机制 2025-03-30 16:14:23 +08:00
liankong
f990bda4e5 绕过CFI限制 2025-03-30 15:37:09 +08:00
liankong
b755ad3602 2 2025-03-30 15:22:40 +08:00
liankong
b060b2827e 1 2025-03-30 15:13:16 +08:00
liankong
73493b288f 按照KernelPatch原代码修复 2025-03-30 14:58:08 +08:00
liankong
87640fb824 3 2025-03-30 02:56:54 +08:00
liankong
fb0a48f9db Merge branch 'dev' of https://github.com/ShirkNeko/SukiSU-Ultra into dev 2025-03-30 02:51:30 +08:00
liankong
acc8670aa9 2 2025-03-30 02:51:20 +08:00
282 changed files with 43546 additions and 11415 deletions

5
.github/FUNDING.yml vendored
View File

@@ -1,5 +1,4 @@
# These are supported funding model platforms
github: tiann
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

@@ -1,28 +0,0 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
groups:
actions:
patterns:
- "*"
- package-ecosystem: cargo
directory: userspace/ksud
schedule:
interval: daily
allow:
- dependency-type: "all"
groups:
crates:
patterns:
- "*"
- package-ecosystem: gradle
directory: manager
schedule:
interval: daily
groups:
maven:
patterns:
- "*"

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

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

View File

@@ -3,9 +3,9 @@ on:
workflow_call:
inputs:
upload:
required: false
required: true
type: boolean
default: false
default: true
description: "Whether to upload to branch"
secrets:
# username:github_pat
@@ -14,9 +14,9 @@ on:
workflow_dispatch:
inputs:
upload:
required: false
required: true
type: boolean
default: false
default: true
description: "Whether to upload to branch"
jobs:
build-lkm:
@@ -24,23 +24,23 @@ jobs:
matrix:
include:
- version: "android12-5.10"
sub_level: 233
os_patch_level: 2025-02
sub_level: 237
os_patch_level: 2025-06
- version: "android13-5.10"
sub_level: 228
os_patch_level: 2025-01
sub_level: 236
os_patch_level: 2025-05
- version: "android13-5.15"
sub_level: 170
os_patch_level: 2025-01
sub_level: 180
os_patch_level: 2025-05
- version: "android14-5.15"
sub_level: 170
os_patch_level: 2025-01
sub_level: 180
os_patch_level: 2025-05
- version: "android14-6.1"
sub_level: 128
os_patch_level: 2025-03
sub_level: 138
os_patch_level: 2025-06
- version: "android15-6.6"
sub_level: 77
os_patch_level: 2025-03
sub_level: 89
os_patch_level: 2025-06
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging
uses: ./.github/workflows/gki-kernel.yml
with:

View File

@@ -0,0 +1,252 @@
name: Build Manager Manual
on:
workflow_dispatch:
inputs:
build_lkm:
required: true
type: choice
default: "auto"
options:
- "true"
- "false"
- "auto"
description: "Whether to build lkm"
upload_lkm:
required: true
type: boolean
default: true
description: "Whether to upload lkm"
jobs:
check-build-lkm:
runs-on: self-hosted
outputs:
build_lkm: ${{ steps.check-build.outputs.build_lkm }}
upload_lkm: ${{ steps.check-build.outputs.upload_lkm }}
steps:
- name: check build
id: check-build
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then
kernel_changed="${{ inputs.build_lkm }}"
else
kernel_changed=true
mkdir tmp
cd tmp
git config --global init.defaultBranch bot
git config --global user.name 'Bot'
git config --global user.email 'bot@github.shirkneko.io'
git init .
git remote add origin https://github.com/${{ github.repository }}
CURRENT_COMMIT="${{ github.event.head_commit.id }}"
git fetch origin $CURRENT_COMMIT --depth=1
git fetch origin lkm --depth=1
LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)"
LKM_COMMIT="${LKM_COMMIT#Upload LKM from }"
LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]')
echo "LKM_COMMIT=$LKM_COMMIT"
git fetch origin "$LKM_COMMIT" --depth=1
git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm-local.yml .github/workflows/build-kernel-*.yml && kernel_changed=false
cd ..
rm -rf tmp
fi
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
need_upload=true
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
need_upload="${{ inputs.upload_lkm }}"
else
need_upload=false
fi
echo "kernel changed: $kernel_changed"
echo "need upload: $need_upload"
echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT"
echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT"
build-lkm:
needs: check-build-lkm
uses: ./.github/workflows/build-lkm-local.yml
if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }}
with:
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
secrets: inherit
build-susfs:
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
matrix:
include:
- target: aarch64-linux-android
os: ubuntu-latest
uses: ./.github/workflows/susfs.yml
with:
target: ${{ matrix.target }}
os: ${{ matrix.os }}
build-kpmmgr:
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
matrix:
include:
- target: aarch64-linux-android
os: ubuntu-latest
uses: ./.github/workflows/kpmmgr.yml
with:
target: ${{ matrix.target }}
os: ${{ matrix.os }}
build-ksud:
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
matrix:
include:
- target: aarch64-linux-android
os: ubuntu-latest
- target: x86_64-linux-android
os: ubuntu-latest
- target: armv7-linux-androideabi
os: ubuntu-latest
uses: ./.github/workflows/ksud.yml
with:
target: ${{ matrix.target }}
os: ${{ matrix.os }}
pack_lkm: true
pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }}
build-manager:
if: ${{ always() }}
needs: build-ksud
runs-on: self-hosted
defaults:
run:
working-directory: ./manager
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup need_upload
id: need_upload
run: |
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
echo "UPLOAD=true" >> $GITHUB_OUTPUT
else
echo "UPLOAD=false" >> $GITHUB_OUTPUT
fi
- name: Write key
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref == 'refs/heads/susfs' || github.ref_type == 'tag' }}
run: |
if [ ! -z "${{ secrets.KEYSTORE }}" ]; then
{
echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'
echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}'
echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'
echo KEYSTORE_FILE='key.jks'
} >> gradle.properties
echo "${{ secrets.KEYSTORE }}" | base64 -d > key.jks
fi
- name: Download arm64 susfs
uses: actions/download-artifact@v4
with:
name: susfs-aarch64-linux-android
path: .
- name: Download arm64 kpmmgr
uses: actions/download-artifact@v4
with:
name: kpmmgr-aarch64-linux-android
path: .
- name: Download arm64 ksud
uses: actions/download-artifact@v4
with:
name: ksud-aarch64-linux-android
path: .
- name: Download x86_64 ksud
uses: actions/download-artifact@v4
with:
name: ksud-x86_64-linux-android
path: .
- name: Download arm ksud
uses: actions/download-artifact@v4
with:
name: ksud-armv7-linux-androideabi
path: .
- name: Copy ksud to app jniLibs
run: |
mkdir -p app/src/main/jniLibs/arm64-v8a
mkdir -p app/src/main/jniLibs/x86_64
mkdir -p app/src/main/jniLibs/armeabi-v7a
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
- name: Copy kpmmgr to app jniLibs
run: |
mkdir -p app/src/main/jniLibs/arm64-v8a
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
- name: Copy susfs to app jniLibs
run: |
mkdir -p app/src/main/jniLibs/arm64-v8a
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
- name: Build with Gradle
run: |
export ANDROID_HOME=/root/.android/sdk
export PATH=$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$PATH
{
echo 'org.gradle.parallel=true'
echo 'org.gradle.vfs.watch=true'
echo 'org.gradle.jvmargs=-Xmx2048m'
echo 'android.native.buildOutput=verbose'
} >> gradle.properties
sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties
./gradlew clean assembleRelease
- name: Upload build artifact
uses: actions/upload-artifact@v4
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
with:
name: manager
path: manager/app/build/outputs/apk/release/*.apk
- name: Upload mappings
uses: actions/upload-artifact@v4
if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }}
with:
name: "mappings"
path: "manager/app/build/outputs/mapping/release/"
- name: Bot session cache
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
id: bot_session_cache
uses: actions/cache@v4
with:
path: scripts/ksubot.session
key: ${{ runner.os }}-bot-session
- name: Upload to telegram
if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'
env:
CHAT_ID: ${{ vars.CHAT_ID }}
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
MESSAGE_THREAD_ID: ${{ vars.MESSAGE_THREAD_ID }}
COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
COMMIT_URL: ${{ github.event.head_commit.url }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
TITLE: Manager
run: |
if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then
export VERSION=$(git rev-list --count HEAD)
APK=$(find ./app/build/outputs/apk/release -name "*.apk")
python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK
fi

View File

@@ -8,7 +8,7 @@ on:
- 'manager/**'
- 'kernel/**'
- 'userspace/ksud/**'
- 'userspace/zakomksd/**'
- 'userspace/susfs/**'
- 'userspace/kpmmgr/**'
pull_request:
branches: [ "main" ]
@@ -29,7 +29,7 @@ on:
upload_lkm:
required: true
type: boolean
default: false
default: true
description: "Whether to upload lkm"
jobs:
check-build-lkm:
@@ -64,7 +64,7 @@ jobs:
cd ..
rm -rf tmp
fi
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/susfs' ]; then
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then
need_upload=true
elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
need_upload="${{ inputs.upload_lkm }}"
@@ -83,7 +83,7 @@ jobs:
with:
upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }}
secrets: inherit
build-zakomksd:
build-susfs:
if: ${{ always() }}
needs: [ check-build-lkm, build-lkm ]
strategy:
@@ -91,7 +91,7 @@ jobs:
include:
- target: aarch64-linux-android
os: ubuntu-latest
uses: ./.github/workflows/zakomksd.yml
uses: ./.github/workflows/susfs.yml
with:
target: ${{ matrix.target }}
os: ${{ matrix.os }}
@@ -119,6 +119,8 @@ jobs:
os: ubuntu-latest
- target: x86_64-linux-android
os: ubuntu-latest
- target: armv7-linux-androideabi
os: ubuntu-latest
uses: ./.github/workflows/ksud.yml
with:
target: ${{ matrix.target }}
@@ -174,10 +176,10 @@ jobs:
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Download arm64 zakomksd
- name: Download arm64 susfs
uses: actions/download-artifact@v4
with:
name: zakomksd-aarch64-linux-android
name: susfs-aarch64-linux-android
path: .
- name: Download arm64 kpmmgr
@@ -198,22 +200,30 @@ jobs:
name: ksud-x86_64-linux-android
path: .
- name: Download arm ksud
uses: actions/download-artifact@v4
with:
name: ksud-armv7-linux-androideabi
path: .
- name: Copy ksud to app jniLibs
run: |
mkdir -p app/src/main/jniLibs/arm64-v8a
mkdir -p app/src/main/jniLibs/x86_64
cp -f ../aarch64-linux-android/release/zakomk ../manager/app/src/main/jniLibs/arm64-v8a/libzakomk.so
cp -f ../x86_64-linux-android/release/zakomk ../manager/app/src/main/jniLibs/x86_64/libzakomk.so
mkdir -p app/src/main/jniLibs/armeabi-v7a
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
- name: Copy kpmmgr to app jniLibs
run: |
mkdir -p app/src/main/jniLibs/arm64-v8a
cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so
- name: Copy zakomksd to app jniLibs
- name: Copy susfs to app jniLibs
run: |
mkdir -p app/src/main/jniLibs/arm64-v8a
cp -f ../arm64-v8a/zakomksd ../manager/app/src/main/jniLibs/arm64-v8a/libzakomksd.so
cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so
- name: Build with Gradle
run: |

40
.github/workflows/crowdin.yml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: Crowdin Action
on:
push:
branches: [ main ]
paths:
- 'manager/app/src/main/res/values/strings.xml'
- 'manager/app/src/main/res/values-*/strings.xml'
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
synchronize-with-crowdin:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Crowdin Action
uses: crowdin/github-action@v2
with:
upload_sources: true
upload_translations: true
auto_approve_imported: true
download_translations: true
skip_untranslated_files: false
skip_untranslated_strings: true
create_pull_request: true
localization_branch_name: "Crowdin"
pull_request_labels: 'enhancement, translation'
pull_request_title: 'opt: sync translation from Crowdin'
config: 'crowdin.yml'
crowdin_branch_name: "main"
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}

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

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

View File

@@ -198,6 +198,9 @@ jobs:
- name: Make working directory clean to avoid dirty
working-directory: android-kernel
run: |
if [ -e common/BUILD.bazel ]; then
sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel
fi
rm common/android/abi_gki_protected_exports_* || echo "No protected exports!"
git config --global user.email "bot@kernelsu.org"
git config --global user.name "KernelSUBot"

View File

@@ -71,4 +71,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: ksud-${{ inputs.target }}
path: userspace/ksud/target/**/release/zakomk*
path: userspace/ksud/target/**/release/zakozako*

View File

@@ -1,11 +1,11 @@
name: Build zakomksd
name: Build susfs
on:
push:
branches: [ "mian" ]
paths:
- '.github/workflows/zakomksd.yml'
- 'userspace/zakomksd/**'
- '.github/workflows/susfs.yml'
- 'userspace/susfs/**'
workflow_dispatch:
workflow_call:
inputs:
@@ -19,7 +19,7 @@ on:
jobs:
build-susfs:
name: Build userspace zakomksd
name: Build userspace susfs
runs-on: ubuntu-latest
steps:
@@ -28,13 +28,13 @@ jobs:
with:
fetch-depth: 0
- name: Build zakomksd
working-directory: ./userspace/zakomksd
- name: Build susfs
working-directory: ./userspace/susfs
run: |
$ANDROID_NDK_HOME/ndk-build
- name: Upload a Build Artifact
uses: actions/upload-artifact@v4
with:
name: zakomksd-aarch64-linux-android
path: ./userspace/zakomksd/libs
name: susfs-aarch64-linux-android
path: ./userspace/susfs/libs

1
.gitignore vendored
View File

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

6
crowdin.yml Normal file
View File

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

View File

@@ -1,101 +0,0 @@
# SukiSU
**Enlish** | [简体中文](README.md)
Android device root solution based on [KernelSU](https://github.com/KernelSU/KernelSU)
**Experimental! Use at your own risk! **This solution is based on [KernelSU]() and is experimental!
>
> This is an unofficial fork, all rights reserved [@tiann](https://github.com/tiann)
>
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
## How to add
Using the susfs-dev branch (integrated susfs with support for non-GKI devices)
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
Use main branching (no longer with support for non-GKI devices)
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
```
## How to use integrated susfs
Use the susfs-dev branch directly without any patching
## More links
Projects compiled based on Sukisu and susfs
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## Hook method
- This method references the hook manual to (https://github.com/rsuntk/KernelSU)
1. **KPROBES hook:**
- This fork only supports GKI (5.10 - 6.x) kernels, all non-GKI kernels must use manual hooks.
- For Loadable Kernel Modules (LKM)
- Default hooking method for GKI kernels
- Requires `CONFIG_KPROBES=y`. 2.
2. **Hooks manual:**
- For GKI (5.10 - 6.x) kernels, add `CONFIG_KSU_MANUAL_HOOK=y` to the kernel defconfig and make sure to protect KernelSU hooks by using `#ifdef CONFIG_KSU_MANUAL_HOOK` instead of `#ifdef CONFIG_KSU`.
- Standard KernelSU hooks: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx syscall hooks: https://github.com/backslashxx/KernelSU/issues/5
- Some non-GKI devices that manually integrate KPROBES do not require the manual VFS hook `new_hook.patch` patch
## Usage
[GKI]
1. such as millet redmi samsung and other devices (does not include the magic kernel manufacturers such as: meizu, a plus real me oppo)
2. find more links in the GKI build project to find the device kernel version directly download with TWRP or kernel flashing tool to brush into the zip with AnyKernel3 suffix can be
3. General without the suffix of the .zip compressed package is universal, gz suffix for the special TianGui models, lz4 suffix for Google models, general brush without the suffix can be!
[OnePlus]
1. Find the Yiga project in the More link and fill in your own, then build it with cloud compilation, and finally brush in the zip with AnyKernel3 suffix.
Note: You only need to fill in the first two kernel versions, such as 5.10, 5.15, 6.1, 6.6.
- Please search for the processor codename by yourself, usually it is all English without numbers.
- Branching and configuration files, please fill in the kernel open source address.
## Features
1. Kernel-based `su` and root access management.
2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system. 3.
3. [Application Profiles](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage. 4.
4. Bringing back non-GKI/GKI 1.0 support
5. More customization
## License
- The file in the “kernel” directory is [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).
- All other parts except the “kernel” directory are [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html).
## 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
How the above list does not have your name, I will keep you updated, 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)Re-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 skills.

View File

@@ -1,103 +1,101 @@
# SukiSU
<img align='right' src='zakomonochrome-128.svg' width='100px' alt="logo">
**简体中文** | [English](README-en.md)
# SukiSU Ultra
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
**English** | [简体中文](./zh/README.md) | [日本語](./ja/README.md) | [Türkçe](./tr/README.md)
**实验性!使用风险自负!**
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)
## Features
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
- 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.
- 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.
## Installation
See [`guide/installation.md`](guide/installation.md)
## Integration
See [`guide/how-to-integrate.md`](guide/how-to-integrate.md)
## Translation
If you need to submit a translation for the manager, please go to [Crowdin](https://crowdin.com/project/SukiSU-Ultra).
## KPM Support
- Based on KernelPatch, we removed features redundant with KSU and retained only KPM support.
- Work in Progress: Expanding APatch compatibility by integrating additional functions to ensure compatibility across different implementations.
**Open-source repository**: [https://github.com/ShirkNeko/SukiSU_KernelPatch_patch](https://github.com/ShirkNeko/SukiSU_KernelPatch_patch)
**KPM template**: [https://github.com/udochina/KPM-Build-Anywhere](https://github.com/udochina/KPM-Build-Anywhere)
> [!Note]
>
> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann)
>
> 1. Requires `CONFIG_KPM=y`
> 2. Non-GKI devices requires `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y`
> 3. For kernels below `4.19`, backporting from `set_memory.h` from `4.19` is required.
## Troubleshooting
## 如何添加
在内核源码的根目录下执行以下命令:
1. Device stuck upon manager app uninstallation?
Uninstall _com.sony.playmemories.mobile_
使用 susfs-dev 分支已集成susfs带非GKI设备的支持
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev
```
## Sponsor
使用 main 分支不再带非GKI设备的支持
```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
```
- [ShirkNeko](https://afdian.com/a/shirkneko) (maintainer of SukiSU)
- [weishu](https://github.com/sponsors/tiann) (author of KernelSU)
## 如何集成 susfs
## ShirkNeko's sponsorship list
1. 直接使用 susfs-dev 分支,不需要再集成 susfs
- [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
## License
## 钩子方法
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
- 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.
1. **KPROBES 钩子:**
- 此方法仅支持 GKI 2.05.10 - 6.x内核,所有非 GKI 2.0 内核都必须使用手动钩子
- 用于可加载内核模块 (LKM)
- GKI 2.0 内核的默认钩子方法
- 需要 `CONFIG_KPROBES=y`
2. **手动钩子:**
- 对于 GKI 2.05.10 - 6.x内核需要在对应设备的 defconfig 文件中添加 `CONFIG_KSU_MANUAL_HOOK=y` 并确保使用 `#ifdef CONFIG_KSU_MANUAL_HOOK` 而不是 `#ifdef CONFIG_KSU` 来保护 KernelSU 钩子
- 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5
- 部分手动集成KPROBES的非 GKI 2.0 设备不需要手动 VFS 钩子 `new_hook.patch` 补丁
## Credit
- [KernelSU](https://github.com/tiann/KernelSU): upstream
- [MKSU](https://github.com/5ec1cff/KernelSU): Magic Mount
- [RKSU](https://github.com/rsuntk/KernelsU): support non-GKI
- [susfs](https://gitlab.com/simonpunk/susfs4ksu): An addon root hiding kernel patches and userspace module for KernelSU.
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch is a key part of the APatch implementation of the kernel module
## 更多链接
基于 SukiSU 和 susfs 编译的项目
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
<details>
<summary>KernelSU's credit</summary>
## 使用方法
### GKI
1. 适用于如小米红米三星等的 GKI 2.0 的设备(不包含魔改内核的厂商如魅族、一加、真我和 oppo
2. 找到更多链接里的 GKI 构建的项目找到设备内核版本直接下载用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可
3. 一般不带后缀的 .zip 压缩包是通用gz 后缀的为天玑机型专用lz4 后缀的为谷歌系机型专用,一般刷不带后缀的即可
### 一加
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
注意事项:
- 内核版本只需要填写前两位即可,如 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 内核的支持仅限susfs-dev和未进行susfs补丁的dev分支
5. 更多自定义功能
## 许可证
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
-`kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
## 赞助名单
- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持
- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
如何以上名单没有你的名称,我会及时更新,再次感谢大家的支持
## 贡献
- [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 技能
- [Kernel-Assisted Superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): The KernelSU idea.
- [Magisk](https://github.com/topjohnwu/Magisk): The powerful root tool.
- [genuine](https://github.com/brevent/genuine/): APK v2 signature validation.
- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills.
</details>

View File

@@ -0,0 +1,91 @@
# 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)
<!-- 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.

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

149
docs/tr/README.md Normal file
View File

@@ -0,0 +1,149 @@
# SukiSU Ultra
[English](../README.md) | [简体中文](../zh/README.md) | [日本語](../ja/README.md) | **Türkçe**
[KernelSU](https://github.com/tiann/KernelSU) tabanlı Android cihaz root çözümü
**Deneysel! Kullanım riski size aittir!**
> Bu resmi olmayan bir daldır, tüm hakları saklıdır [@tiann](https://github.com/tiann)
>
> Ancak, gelecekte ayrı bir KSU dalı olarak devam edeceğiz
## Nasıl Eklenir
Çekirdek kaynak kodunun kök dizininde aşağıdaki komutları çalıştırın:
Ana dalı kullanın (GKI olmayan cihazlar için desteklenmez)
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
```
GKI olmayan cihazları destekleyen dalı kullanın
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
```
## susfs Nasıl Entegre Edilir
1. Doğrudan susfs-main veya susfs-\* dalını kullanın, susfs entegrasyonuna gerek yok
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
```
## Kanca Yöntemleri
- Bu bölüm [rsuntk\'nin kanca yöntemlerinden](https://github.com/rsuntk/KernelSU) alıntılanmıştır
1. **KPROBES Kancası:**
- Yüklenebilir çekirdek modülleri (LKM) için kullanılır
- GKI 2.0 çekirdeğinin varsayılan kanca yöntemi
- `CONFIG_KPROBES=y` gerektirir
2. **Manuel Kanca:**
- Standart KernelSU kancası: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx\'nin syscall manuel kancası: https://github.com/backslashxx/KernelSU/issues/5
- GKI olmayan çekirdeğin varsayılan kanca yöntemi
- `CONFIG_KSU_MANUAL_HOOK=y` gerektirir
## KPM Desteği
- KernelPatch tabanlı olarak KSU ile çakışan işlevleri kaldırdık ve yalnızca KPM desteğini koruduk
- APatch ile daha fazla uyumlu fonksiyon ekleyerek KPM işlevlerinin bütünlüğünü sağlayacağız
Kaynak kodu: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM şablonu: https://github.com/udochina/KPM-Build-Anywhere
> [!Note]
>
> 1. `CONFIG_KPM=y` gerektirir
> 2. GKI olmayan cihazlar ayrıca `CONFIG_KALLSYMS=y` ve `CONFIG_KALLSYMS_ALL=y` gerektirir
> 3. Bazı çekirdek `4.19` altı kaynak kodları, `4.19`dan geri taşınan başlık dosyası `set_memory.h` gerektirir
## Sistem Güncellemesini Yaparak ROOT\'u Koruma
- OTA\'dan sonra hemen yeniden başlatmayın, yöneticiye girin ve çekirdek yazma/onarma arayüzüne gidin, `GKI/non_GKI yükleme` seçeneğini bulun ve Anykernel3 çekirdek sıkıştırma dosyasını seçin, şu anda sistemin çalıştığı yuva ile zıt yuvaya yazın ve yeniden başlatın, böylece GKI modu güncellemesini koruyabilirsiniz (şu anda tüm GKI olmayan cihazlar bu yöntemi desteklemiyor, lütfen kendiniz deneyin. GKI olmayan cihazlar için TWRP kullanmak en güvenlidir)
- Veya kullanılmayan yuvaya LKM modunu kullanarak yükleyin (OTA\'dan sonra)
## Uyumluluk Durumu
- KernelSU (v1.0.0 öncesi sürümler) resmi olarak Android GKI 2.0 cihazlarını destekler (çekirdek 5.10+)
- Eski çekirdekler (4.4+) de uyumludur, ancak çekirdeği manuel olarak oluşturmanız gerekir
- Daha fazla geri taşımayla KernelSU, 3.x çekirdeğini (3.4-3.18) destekleyebilir
- Şu anda `arm64-v8a`, `armeabi-v7a (bare)` ve bazı `X86_64` desteklenmektedir
## Daha Fazla Bağlantı
SukiSU ve susfs tabanlı derlenen projeler
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## Kullanım Yöntemi
### Evrensel GKI
Lütfen **tümünü** https://kernelsu.org/zh_CN/guide/installation.html adresinden inceleyin
> [!Note]
>
> 1. Xiaomi, Redmi, Samsung gibi GKI 2.0 cihazlar için uygundur (Meizu, OnePlus, Realme ve Oppo gibi değiştirilmiş çekirdekli üreticiler hariç)
> 2. [Daha fazla bağlantı](#daha-fazla-bağlantı) bölümündeki GKI tabanlı projeleri bulun. Cihaz çekirdek sürümünü bulun. Ardından indirin ve TWRP veya çekirdek yazma aracı kullanarak AnyKernel3 soneki olan sıkıştırılmış paketi yazın
> 3. Genellikle sonek olmayan .zip sıkıştırılmış paketler sıkıştırılmamıştır, gz soneki olanlar ise Dimensity modelleri için kullanılan sıkıştırma yöntemidir
### OnePlus
1. Daha fazla bağlantı bölümündeki OnePlus projesini bulun ve kendiniz doldurun, ardından bulut derleme yapın ve AnyKernel3 soneki olan sıkıştırılmış paketi yazın
> [!Note]
>
> - Çekirdek sürümü için yalnızca ilk iki haneyi doldurmanız yeterlidir, örneğin 5.10, 5.15, 6.1, 6.6
> - İşlemci kod adını kendiniz arayın, genellikle tamamen İngilizce ve sayı içermeden oluşur
> - Dal ve yapılandırma dosyasını kendiniz OnePlus çekirdek kaynak kodundan doldurun
## Özellikler
1. Çekirdek tabanlı `su` ve root erişim yönetimi
2. 5ec1cff\'nin [Magic Mount](https://github.com/5ec1cff/KernelSU) tabanlı modül sistemi
3. [App Profile](https://kernelsu.org/guide/app-profile.html): root yetkilerini kafeste kilitleyin
4. GKI 2.0 olmayan çekirdekler için desteğin geri getirilmesi
5. Daha fazla özelleştirme özelliği
6. KPM çekirdek modülleri için destek
## Lisans
- `kernel` dizinindeki dosyalar [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır.
- Anime karakter ifadeleri içeren `ic_launcher(?!.*alt.*).*` dosyalarının görüntüleri [怡子曰曰](https://space.bilibili.com/10545509) tarafından telif hakkıyla korunmaktadır, görüntülerdeki Marka Fikri Mülkiyeti [明风 OuO](https://space.bilibili.com/274939213)'ye aittir ve vektörleştirme @MiRinChan tarafından yapılmıştır. Bu dosyaları kullanmadan önce, [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) ile uyumlu olmanın yanı sıra, bu sanatsal içerikleri kullanmak için iki yazarın yetkilendirmesine de uymanız gerekir.
- Yukarıda belirtilen dosyalar veya dizinler hariç, diğer tüm parçalar [GPL-3.0 veya üzeri](https://www.gnu.org/licenses/gpl-3.0.html)'dir.
## Afdian Bağlantısı
- https://afdian.com/a/shirkneko
## Sponsor Listesi
- [Ktouls](https://github.com/Ktouls) Bana sağladığınız destek için çok teşekkür ederim
- [zaoqi123](https://github.com/zaoqi123) Bana sütlü çay ısmarlamanız da güzel
- [wswzgdg](https://github.com/wswzgdg) Bu projeye olan desteğiniz için çok teşekkür ederim
- [yspbwx2010](https://github.com/yspbwx2010) Çok teşekkür ederim
- [DARKWWEE](https://github.com/DARKWWEE) 100 USDT için teşekkürler
## Katkıda Bulunanlar
- [KernelSU](https://github.com/tiann/KernelSU): Orijinal proje
- [MKSU](https://github.com/5ec1cff/KernelSU): Kullanılan proje
- [RKSU](https://github.com/rsuntk/KernelsU): GKI olmayan cihazlar için destek sağlayan proje
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu): Kullanılan susfs dosya sistemi
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri
- [Magisk](https://github.com/topjohnwu/Magisk): Güçlü root aracı
- [genuine](https://github.com/brevent/genuine/): APK v2 imza doğrulama
- [Diamorphine](https://github.com/m0nad/Diamorphine): Bazı rootkit becerileri
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch, APatch\'in çekirdek modüllerini uygulamak için kritik bir parçadır

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,91 @@
# 集成指导
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`](guide/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)
<!-- 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,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

@@ -16,19 +16,42 @@ config KSU_DEBUG
help
Enable KernelSU debug mode.
config KSU_HOOK
bool "Enable KernelSU Hook"
default n
help
This option enables the KernelSU Hook feature. If enabled, it will
override the kernel version check and enable the hook functionality.
config KPM
bool "Enable KernelSU KPM"
bool "Enable SukiSU KPM"
depends on KSU && 64BIT
default n
help
This option enables the KernelSU KPM feature. If enabled, it will
override the kernel version check and enable the hook functionality.
Enabling this option will activate the KPM feature of SukiSU.
This option is suitable for scenarios where you need to force KPM to be enabled.
but it may affect system stability.
select KALLSYMS
select KALLSYMS_ALL
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

View File

@@ -1,5 +1,6 @@
kernelsu-objs := ksu.o
kernelsu-objs += allowlist.o
kernelsu-objs += dynamic_sign.o
kernelsu-objs += apk_sign.o
kernelsu-objs += sucompat.o
kernelsu-objs += throne_tracker.o
@@ -8,6 +9,10 @@ kernelsu-objs += ksud.o
kernelsu-objs += embed_ksud.o
kernelsu-objs += kernel_compat.o
ifeq ($(strip $(CONFIG_KSU_TRACEPOINT_HOOK)),y)
kernelsu-objs += ksu_trace.o
endif
kernelsu-objs += selinux/selinux.o
kernelsu-objs += selinux/sepolicy.o
kernelsu-objs += selinux/rules.o
@@ -15,45 +20,108 @@ 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
obj-$(CONFIG_KSU) += kernelsu.o
obj-$(CONFIG_KSU_TRACEPOINT_HOOK) += ksu_trace_export.o
obj-$(CONFIG_KPM) += kpm/
# .git is a text file while the module is imported by 'git submodule add'.
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow)
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count HEAD)
# ksu_version: major * 10000 + git version + 606 for historical reasons
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606))
$(info -- KernelSU version: $(KSU_VERSION))
REPO_OWNER := SukiSU-Ultra
REPO_NAME := SukiSU-Ultra
REPO_BRANCH := main
KSU_VERSION_API := 3.1.8
GIT_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git
CURL_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin curl
KSU_GITHUB_VERSION := $(shell $(CURL_BIN) -s "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
KSU_GITHUB_VERSION_COMMIT := $(shell $(CURL_BIN) -sI "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/commits?sha=$(REPO_BRANCH)&per_page=1" | grep -i "link:" | sed -n 's/.*page=\([0-9]*\)>; rel="last".*/\1/p')
LOCAL_GIT_EXISTS := $(shell test -e $(srctree)/$(src)/../.git && echo 1 || echo 0)
define get_ksu_version_full
v$1-$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --abbrev-ref HEAD)
endef
ifeq ($(KSU_GITHUB_VERSION_COMMIT),)
ifeq ($(LOCAL_GIT_EXISTS),1)
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
KSU_LOCAL_VERSION := $(shell cd $(srctree)/$(src); $(GIT_BIN) rev-list --count $(REPO_BRANCH))
KSU_VERSION := $(shell expr 10000 + $(KSU_LOCAL_VERSION) + 700)
$(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION))
else
KSU_VERSION := 13000
$(warning -- Could not fetch version online or via local .git! Using fallback version: $(KSU_VERSION))
endif
else
KSU_VERSION := $(shell expr 10000 + $(KSU_GITHUB_VERSION_COMMIT) + 700)
$(info -- $(REPO_NAME) version (GitHub): $(KSU_VERSION))
endif
ifeq ($(KSU_GITHUB_VERSION),)
ifeq ($(LOCAL_GIT_EXISTS),1)
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_VERSION_API))
$(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION_FULL))
$(info -- $(REPO_NAME) Formatted version (local .git): $(KSU_VERSION))
else
KSU_VERSION_FULL := v$(KSU_VERSION_API)-$(REPO_NAME)-unknown@unknown
$(warning -- $(REPO_NAME) version: $(KSU_VERSION_FULL))
endif
else
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_GITHUB_VERSION))
$(info -- $(REPO_NAME) version (Github): $(KSU_VERSION_FULL))
endif
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
else # If there is no .git file, the default version will be passed.
$(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git submodule!")
ccflags-y += -DKSU_VERSION=16
ccflags-y += -DKSU_VERSION_FULL=\"$(KSU_VERSION_FULL)\"
# Custom Signs
ifdef KSU_EXPECTED_SIZE
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
$(info -- Custom KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
endif
ifndef KSU_EXPECTED_SIZE
KSU_EXPECTED_SIZE := 0x35c
endif
ifndef KSU_EXPECTED_HASH
KSU_EXPECTED_HASH := 947ae944f3de4ed4c21a7e4f7953ecf351bfa2b36239da37a34111ad29993eef
ifdef KSU_EXPECTED_HASH
ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\"
$(info -- Custom KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
endif
ifdef KSU_MANAGER_PACKAGE
ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\"
$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))
$(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE))
endif
$(info -- 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
$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))
$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))
$(info -- Supported Unofficial Manager: ShirkNeko (GKI) (Non-GKI))
KERNEL_VERSION := $(VERSION).$(PATCHLEVEL)
KERNEL_TYPE := Non-GKI
# Check for GKI 2.0 (5.10+ or 6.x+)
ifneq ($(shell test \( $(VERSION) -ge 5 -a $(PATCHLEVEL) -ge 10 \) -o $(VERSION) -ge 6; echo $$?),0)
# Check for GKI 1.0 (5.4)
ifeq ($(shell test $(VERSION)-$(PATCHLEVEL) = 5-4; echo $$?),0)
KERNEL_TYPE := GKI 1.0
endif
else
KERNEL_TYPE := GKI 2.0
endif
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
$(info -- KERNEL_TYPE: $(KERNEL_TYPE))
ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)
ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\"
$(info -- KERNEL_VERSION: $(KERNEL_VERSION))
ifeq ($(CONFIG_KPM),y)
$(info -- KPM is enabled)
else
$(info -- KPM is disabled)
endif
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function

View File

@@ -15,15 +15,27 @@
#endif
#include "apk_sign.h"
#include "dynamic_sign.h"
#include "klog.h" // IWYU pragma: keep
#include "kernel_compat.h"
#include "manager_sign.h"
struct sdesc {
struct shash_desc shash;
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)
{
struct sdesc *sdesc;
@@ -70,10 +82,12 @@ static int ksu_sha256(const unsigned char *data, unsigned int datalen,
crypto_free_shash(alg);
return ret;
}
static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset,
unsigned expected_size, const char *expected_sha256)
static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, int *matched_index)
{
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 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
*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_sign_config(&size, &hash)) {
sign_key.size = size;
sign_key.sha256 = hash;
}
}
if (*size4 != sign_key.size)
continue;
*offset += *size4;
#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';
bin2hex(hash_str, digest, SHA256_DIGEST_SIZE);
pr_info("sha256: %s, expected: %s\n", hash_str,
expected_sha256);
if (strcmp(expected_sha256, hash_str) == 0) {
return true;
pr_info("sha256: %s, expected: %s, index: %d\n", hash_str, sign_key.sha256, i);
if (strcmp(sign_key.sha256, hash_str) == 0) {
signature_valid = true;
if (matched_index) {
*matched_index = i;
}
break;
}
}
return false;
return signature_valid;
}
struct zip_entry_header {
@@ -155,8 +186,7 @@ static bool has_v1_signature_file(struct file *fp)
fileName[header.file_name_length] = '\0';
// Check if the entry matches META-INF/MANIFEST.MF
if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) ==
0) {
if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == 0) {
return true;
}
} else {
@@ -171,21 +201,17 @@ static bool has_v1_signature_file(struct file *fp)
return false;
}
static __always_inline bool check_v2_signature(char *path,
unsigned expected_size,
const char *expected_sha256)
static __always_inline bool check_v2_signature(char *path, bool check_multi_manager, int *signature_index)
{
unsigned char buffer[0x11] = { 0 };
u32 size4;
u64 size8, size_of_block;
loff_t pos;
bool v2_signing_valid = false;
int v2_signing_blocks = 0;
bool v3_signing_exist = false;
bool v3_1_signing_exist = false;
int matched_index = -1;
int i;
struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0);
if (IS_ERR(fp)) {
@@ -193,6 +219,12 @@ static __always_inline bool check_v2_signature(char *path,
return false;
}
// If you want to check for multi-manager APK signing, but dynamic signing is not enabled, skip
if (check_multi_manager && !ksu_is_dynamic_sign_enabled()) {
filp_close(fp, 0);
return 0;
}
// disable inotify for this file
fp->f_mode |= FMODE_NONOTIFY;
@@ -244,9 +276,10 @@ static __always_inline bool check_v2_signature(char *path,
offset = 4;
if (id == 0x7109871au) {
v2_signing_blocks++;
v2_signing_valid =
check_block(fp, &size4, &pos, &offset,
expected_size, expected_sha256);
bool result = check_block(fp, &size4, &pos, &offset, &matched_index);
if (result) {
v2_signing_valid = true;
}
} else if (id == 0xf05368c0u) {
// http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73
v3_signing_exist = true;
@@ -287,7 +320,24 @@ clean:
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_sign 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
@@ -316,5 +366,10 @@ module_param_cb(ksu_debug_manager_uid, &expected_size_ops,
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_multi_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
#include <linux/types.h>
#include "ksu.h"
bool is_manager_apk(char *path);

View File

@@ -42,10 +42,10 @@
#include "manager.h"
#include "selinux/selinux.h"
#include "throne_tracker.h"
#include "throne_tracker.h"
#include "kernel_compat.h"
#include "kpm/kpm.h"
#include "dynamic_sign.h"
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);
set_groups(cred, group_info);
put_group_info(group_info);
}
static void disable_seccomp()
@@ -134,18 +135,18 @@ void escape_to_root(void)
{
struct cred *cred;
rcu_read_lock();
do {
cred = (struct cred *)__task_cred((current));
BUG_ON(!cred);
} while (!get_cred_rcu(cred));
cred = prepare_creds();
if (!cred) {
pr_warn("prepare_creds failed!\n");
return;
}
if (cred->euid.val == 0) {
pr_warn("Already root, don't escape!\n");
rcu_read_unlock();
abort_creds(cred);
return;
}
struct root_profile *profile = ksu_get_root_profile(cred->uid.val);
cred->uid.val = profile->uid;
@@ -176,7 +177,7 @@ void escape_to_root(void)
setup_groups(profile, cred);
rcu_read_unlock();
commit_creds(cred);
// Refer to kernel/seccomp.c: seccomp_set_mode_strict
// When disabling Seccomp, ensure that current->sighand->siglock is held during the operation.
@@ -226,6 +227,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
return 0;
}
#ifdef CONFIG_EXT4_FS
static void nuke_ext4_sysfs() {
struct path path;
int err = kern_path("/data/adb/modules", 0, &path);
@@ -242,6 +244,20 @@ static void nuke_ext4_sysfs() {
}
ext4_unregister_sysfs(sb);
path_put(&path);
}
#else
static inline void nuke_ext4_sysfs() { }
#endif
static bool is_system_bin_su()
{
// YES in_execve becomes 0 when it succeeds.
if (!current->mm || current->in_execve)
return false;
// quick af check
return (current->mm->exe_file && !strcmp(current->mm->exe_file->f_path.dentry->d_name.name, "su"));
}
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
@@ -266,7 +282,8 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
bool from_root = 0 == current_uid().val;
bool from_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
return 0;
}
@@ -302,7 +319,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
if (copy_to_user(arg3, &version, sizeof(version))) {
pr_err("prctl reply error, cmd: %lu\n", arg2);
}
u32 version_flags = 0;
u32 version_flags = 2;
#ifdef MODULE
version_flags |= 0x1;
#endif
@@ -313,6 +330,72 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
return 0;
}
// Allow root manager to get full version strings
if (arg2 == CMD_GET_FULL_VERSION) {
char ksu_version_full[KSU_FULL_VERSION_STRING] = {0};
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
strscpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING);
#else
strlcpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING);
#endif
if (copy_to_user((void __user *)arg3, ksu_version_full, KSU_FULL_VERSION_STRING)) {
pr_err("prctl reply error, cmd: %lu\n", arg2);
return -EFAULT;
}
return 0;
}
// Allow the root manager to configure dynamic signatures
if (arg2 == CMD_DYNAMIC_SIGN) {
if (!from_root && !from_manager) {
return 0;
}
struct dynamic_sign_user_config config;
if (copy_from_user(&config, (void __user *)arg3, sizeof(config))) {
pr_err("copy dynamic sign config failed\n");
return 0;
}
int ret = ksu_handle_dynamic_sign(&config);
if (ret == 0 && config.operation == DYNAMIC_SIGN_OP_GET) {
if (copy_to_user((void __user *)arg3, &config, sizeof(config))) {
pr_err("copy dynamic sign config back failed\n");
return 0;
}
}
if (ret == 0) {
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
pr_err("dynamic_sign: 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 (!from_root) {
return 0;
@@ -324,6 +407,9 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
post_fs_data_lock = true;
pr_info("post-fs-data triggered\n");
on_post_fs_data();
// Initializing Dynamic Signatures
ksu_dynamic_sign_init();
pr_info("Dynamic sign config loaded during post-fs-data\n");
}
break;
}
@@ -412,6 +498,30 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
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
// ADD: 添加KPM模块控制
if(sukisu_is_kpm_control_code(arg2)) {
@@ -419,15 +529,41 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
pr_info("KPM: calling before arg2=%d\n", (int) arg2);
res = sukisu_handle_kpm(arg2, arg3, arg4);
copy_to_user(result, &res, sizeof(res));
pr_info("KPM: calling before arg2=%d res=%d\n", (int) arg2, (int) res);
res = sukisu_handle_kpm(arg2, arg3, arg4, arg5);
return 0;
}
#endif
if (arg2 == CMD_ENABLE_KPM) {
bool KPM_Enabled = IS_ENABLED(CONFIG_KPM);
if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled)))
pr_info("KPM: copy_to_user() failed\n");
return 0;
}
// Checking hook usage
if (arg2 == CMD_HOOK_TYPE) {
const char *hook_type = "Kprobes";
#if defined(CONFIG_KSU_TRACEPOINT_HOOK)
hook_type = "Tracepoint";
#elif defined(CONFIG_KSU_MANUAL_HOOK)
hook_type = "Manual";
#endif
size_t len = strlen(hook_type) + 1;
if (copy_to_user((void __user *)arg3, hook_type, len)) {
pr_err("hook_type: copy_to_user failed\n");
return 0;
}
if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {
pr_err("hook_type: prctl reply error\n");
}
return 0;
}
// all other cmds are for 'root manager'
if (!from_manager) {
return 0;
@@ -482,30 +618,6 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
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;
}
@@ -556,11 +668,13 @@ static void try_umount(const char *mnt, bool check_mnt, int flags)
if (path.dentry != path.mnt->mnt_root) {
// it is not root mountpoint, maybe umounted by others already.
path_put(&path);
return;
}
// we are only interest in some specific mounts
if (check_mnt && !should_umount(&path)) {
path_put(&path);
return;
}
@@ -716,6 +830,19 @@ static int ksu_task_fix_setuid(struct cred *new, const struct cred *old,
}
#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[] = {
LSM_HOOK_INIT(task_prctl, ksu_task_prctl),
LSM_HOOK_INIT(inode_rename, ksu_inode_rename),

View File

@@ -2,6 +2,7 @@
#define __KSU_H_KSU_CORE
#include <linux/init.h>
#include "apk_sign.h"
void __init ksu_core_init(void);
void ksu_core_exit(void);

505
kernel/dynamic_sign.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_sign.h"
#include "klog.h" // IWYU pragma: keep
#include "kernel_compat.h"
#include "manager.h"
#define MAX_MANAGERS 2
// Dynamic sign configuration
static struct dynamic_sign_config dynamic_sign = {
.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_sign_lock);
// Work queues for persistent storage
static struct work_struct ksu_save_dynamic_sign_work;
static struct work_struct ksu_load_dynamic_sign_work;
static struct work_struct ksu_clear_dynamic_sign_work;
bool ksu_is_dynamic_sign_enabled(void)
{
unsigned long flags;
bool enabled;
spin_lock_irqsave(&dynamic_sign_lock, flags);
enabled = dynamic_sign.is_set;
spin_unlock_irqrestore(&dynamic_sign_lock, flags);
return enabled;
}
void ksu_add_manager(uid_t uid, int signature_index)
{
unsigned long flags;
int i;
if (!ksu_is_dynamic_sign_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_sign_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_sign_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_sign_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_sign_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_sign(struct work_struct *work)
{
u32 magic = DYNAMIC_SIGN_FILE_MAGIC;
u32 version = DYNAMIC_SIGN_FILE_VERSION;
struct dynamic_sign_config config_to_save;
loff_t off = 0;
unsigned long flags;
struct file *fp;
spin_lock_irqsave(&dynamic_sign_lock, flags);
config_to_save = dynamic_sign;
spin_unlock_irqrestore(&dynamic_sign_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_SIGN, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (IS_ERR(fp)) {
pr_err("save_dynamic_sign 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_sign write magic failed.\n");
goto exit;
}
if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != sizeof(version)) {
pr_err("save_dynamic_sign 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_sign write config failed.\n");
goto exit;
}
pr_info("Dynamic sign config saved successfully\n");
exit:
filp_close(fp, 0);
}
static void do_load_dynamic_sign(struct work_struct *work)
{
loff_t off = 0;
ssize_t ret = 0;
struct file *fp = NULL;
u32 magic;
u32 version;
struct dynamic_sign_config loaded_config;
unsigned long flags;
int i;
fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_SIGN, O_RDONLY, 0);
if (IS_ERR(fp)) {
if (PTR_ERR(fp) == -ENOENT) {
pr_info("No saved dynamic sign config found\n");
} else {
pr_err("load_dynamic_sign open file failed: %ld\n", PTR_ERR(fp));
}
return;
}
if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic) ||
magic != DYNAMIC_SIGN_FILE_MAGIC) {
pr_err("dynamic sign file invalid magic: %x!\n", magic);
goto exit;
}
if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != sizeof(version)) {
pr_err("dynamic sign read version failed\n");
goto exit;
}
pr_info("dynamic sign file version: %d\n", version);
ret = ksu_kernel_read_compat(fp, &loaded_config, sizeof(loaded_config), &off);
if (ret <= 0) {
pr_info("load_dynamic_sign read err: %zd\n", ret);
goto exit;
}
if (ret != sizeof(loaded_config)) {
pr_err("load_dynamic_sign 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_sign_lock, flags);
dynamic_sign = loaded_config;
spin_unlock_irqrestore(&dynamic_sign_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_sign(void)
{
return ksu_queue_work(&ksu_save_dynamic_sign_work);
}
static void do_clear_dynamic_sign(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_SIGN, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (IS_ERR(fp)) {
pr_err("clear_dynamic_sign 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_sign write null bytes failed.\n");
} else {
pr_info("Dynamic sign config file cleared successfully\n");
}
filp_close(fp, 0);
}
static bool clear_dynamic_sign_file(void)
{
return ksu_queue_work(&ksu_clear_dynamic_sign_work);
}
int ksu_handle_dynamic_sign(struct dynamic_sign_user_config *config)
{
unsigned long flags;
int ret = 0;
int i;
if (!config) {
return -EINVAL;
}
switch (config->operation) {
case DYNAMIC_SIGN_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_sign_lock, flags);
dynamic_sign.size = config->size;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
strscpy(dynamic_sign.hash, config->hash, sizeof(dynamic_sign.hash));
#else
strlcpy(dynamic_sign.hash, config->hash, sizeof(dynamic_sign.hash));
#endif
dynamic_sign.is_set = 1;
spin_unlock_irqrestore(&dynamic_sign_lock, flags);
persistent_dynamic_sign();
pr_info("dynamic sign updated: size=0x%x, hash=%.16s... (multi-manager enabled)\n",
config->size, config->hash);
break;
case DYNAMIC_SIGN_OP_GET:
spin_lock_irqsave(&dynamic_sign_lock, flags);
if (dynamic_sign.is_set) {
config->size = dynamic_sign.size;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)
strscpy(config->hash, dynamic_sign.hash, sizeof(config->hash));
#else
strlcpy(config->hash, dynamic_sign.hash, sizeof(config->hash));
#endif
ret = 0;
} else {
ret = -ENODATA;
}
spin_unlock_irqrestore(&dynamic_sign_lock, flags);
break;
case DYNAMIC_SIGN_OP_CLEAR:
spin_lock_irqsave(&dynamic_sign_lock, flags);
dynamic_sign.size = 0x300;
strcpy(dynamic_sign.hash, "0000000000000000000000000000000000000000000000000000000000000000");
dynamic_sign.is_set = 0;
spin_unlock_irqrestore(&dynamic_sign_lock, flags);
// Clear only dynamic managers, preserve default manager
clear_dynamic_manager();
// Clear file using the same method as save
clear_dynamic_sign_file();
pr_info("Dynamic sign config cleared (multi-manager disabled)\n");
break;
default:
pr_err("Invalid dynamic sign operation: %d\n", config->operation);
return -EINVAL;
}
return ret;
}
bool ksu_load_dynamic_sign(void)
{
return ksu_queue_work(&ksu_load_dynamic_sign_work);
}
void ksu_dynamic_sign_init(void)
{
int i;
INIT_WORK(&ksu_save_dynamic_sign_work, do_save_dynamic_sign);
INIT_WORK(&ksu_load_dynamic_sign_work, do_load_dynamic_sign);
INIT_WORK(&ksu_clear_dynamic_sign_work, do_clear_dynamic_sign);
// Initialize manager slots
for (i = 0; i < MAX_MANAGERS; i++) {
active_managers[i].is_active = false;
}
ksu_load_dynamic_sign();
pr_info("Dynamic sign initialized with conditional multi-manager support\n");
}
void ksu_dynamic_sign_exit(void)
{
clear_dynamic_manager();
// Save current config before exit
do_save_dynamic_sign(NULL);
pr_info("Dynamic sign exited with persistent storage\n");
}
// Get dynamic sign configuration for signature verification
bool ksu_get_dynamic_sign_config(unsigned int *size, const char **hash)
{
unsigned long flags;
bool valid = false;
spin_lock_irqsave(&dynamic_sign_lock, flags);
if (dynamic_sign.is_set) {
if (size) *size = dynamic_sign.size;
if (hash) *hash = dynamic_sign.hash;
valid = true;
}
spin_unlock_irqrestore(&dynamic_sign_lock, flags);
return valid;
}

43
kernel/dynamic_sign.h Normal file
View File

@@ -0,0 +1,43 @@
#ifndef __KSU_H_DYNAMIC_SIGN
#define __KSU_H_DYNAMIC_SIGN
#include <linux/types.h>
#include "ksu.h"
#define DYNAMIC_SIGN_FILE_MAGIC 0x7f445347 // 'DSG', u32
#define DYNAMIC_SIGN_FILE_VERSION 1 // u32
#define KERNEL_SU_DYNAMIC_SIGN "/data/adb/ksu/.dynamic_sign"
struct dynamic_sign_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
int ksu_handle_dynamic_sign(struct dynamic_sign_user_config *config);
void ksu_dynamic_sign_init(void);
void ksu_dynamic_sign_exit(void);
bool ksu_load_dynamic_sign(void);
bool ksu_is_dynamic_sign_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_multi_manager_apk(char *path, int *signature_index);
// Configuration access for signature verification
bool ksu_get_dynamic_sign_config(unsigned int *size, const char **hash);
#endif

View File

@@ -1,2 +1,6 @@
obj-y += kpm.o
obj-y += compact.o
obj-y += super_access.o
ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat
ccflags-y += -Wno-declaration-after-statement -Wno-unused-function

View File

@@ -26,37 +26,64 @@
#include <linux/slab.h>
#include "kpm.h"
#include "compact.h"
#include "../allowlist.h"
#include "../manager.h"
unsigned long sukisu_compact_find_symbol(const char* name);
// ======================================================================
// 兼容函数 for KPM
const char* kpver = "0.10";
static
int sukisu_is_su_allow_uid(uid_t uid) {
return ksu_is_allow_uid(uid) ? 1 : 0;
}
static
int sukisu_get_ap_mod_exclude(uid_t uid) {
// Not supported
return 0;
}
static
int sukisu_is_uid_should_umount(uid_t uid) {
return ksu_uid_should_umount(uid) ? 1 : 0;
}
static
int sukisu_is_current_uid_manager() {
return is_manager();
}
static
uid_t sukisu_get_manager_uid() {
return ksu_manager_uid;
}
static
void sukisu_set_manager_uid(uid_t uid, int force) {
if(force || ksu_manager_uid == -1) {
ksu_manager_uid = uid;
}
}
// ======================================================================
struct CompactAddressSymbol {
const char* symbol_name;
void* addr;
};
struct CompactAliasSymbol {
const char* symbol_name;
const char* compact_symbol_name;
};
struct CompactAddressSymbol address_symbol [] = {
static struct CompactAddressSymbol address_symbol [] = {
{ "kallsyms_lookup_name", &kallsyms_lookup_name },
{ "compact_find_symbol", &sukisu_compact_find_symbol },
{ "compat_copy_to_user", &copy_to_user },
{ "compat_strncpy_from_user", &strncpy_from_user },
{ "kpver", &kpver },
{ "is_run_in_sukisu_ultra", (void*)1 }
};
struct CompactAliasSymbol alias_symbol[] = {
{"kf_strncat", "strncat"},
{"kf_strlen", "strlen" },
{"kf_strcpy", "strcpy"},
{"compat_copy_to_user", "__arch_copy_to_user"}
{ "is_run_in_sukisu_ultra", (void*)1 },
{ "is_su_allow_uid", &sukisu_is_su_allow_uid },
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
{ "is_uid_should_umount", &sukisu_is_uid_should_umount },
{ "is_current_uid_manager", &sukisu_is_current_uid_manager },
{ "get_manager_uid", &sukisu_get_manager_uid },
{ "sukisu_set_manager_uid", &sukisu_set_manager_uid }
};
unsigned long sukisu_compact_find_symbol(const char* name) {
@@ -71,30 +98,13 @@ unsigned long sukisu_compact_find_symbol(const char* name) {
}
}
/* 如果符号名以 "kf__" 开头,尝试解析去掉前缀的部分 */
if (strncmp(name, "kf__", 4) == 0) {
const char *real_name = name + 4; // 去掉 "kf__"
addr = (unsigned long)kallsyms_lookup_name(real_name);
if (addr) {
return addr;
}
}
// 通过内核来查
addr = kallsyms_lookup_name(name);
if(addr) {
return addr;
}
// 查不到就查查兼容的符号
for(i = 0; i < (sizeof(alias_symbol) / sizeof(struct CompactAliasSymbol)); i++) {
struct CompactAliasSymbol* symbol = &alias_symbol[i];
if(strcmp(name, symbol->symbol_name) == 0) {
addr = kallsyms_lookup_name(symbol->compact_symbol_name);
if(addr)
return addr;
}
}
return 0;
}
EXPORT_SYMBOL(sukisu_compact_find_symbol);

File diff suppressed because it is too large Load Diff

View File

@@ -1,44 +1,44 @@
#ifndef ___SUKISU_KPM_H
#define ___SUKISU_KPM_H
int sukisu_handle_kpm(unsigned long arg3, unsigned long arg4, unsigned long arg5);
int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, unsigned long arg5);
int sukisu_is_kpm_control_code(unsigned long arg2);
// KPM控制代码
#define CMD_KPM_CONTROL 28
#define CMD_KPM_CONTROL_MAX 34
#define CMD_KPM_CONTROL_MAX 35
// 控制代码
// prctl(xxx, xxx, 1, "PATH", "ARGS")
// prctl(xxx, 28, "PATH", "ARGS")
// success return 0, error return -N
#define SUKISU_KPM_LOAD 28
// prctl(xxx, xxx, 2, "NAME")
// prctl(xxx, 29, "NAME")
// success return 0, error return -N
#define SUKISU_KPM_UNLOAD 29
// num = prctl(xxx, xxx, 3)
// num = prctl(xxx, 30)
// error return -N
// success return +num or 0
#define SUKISU_KPM_NUM 30
// prctl(xxx, xxx, 4, Buffer, BufferSize)
// prctl(xxx, 31, Buffer, BufferSize)
// success return +out, error return -N
#define SUKISU_KPM_LIST 31
// prctl(xxx, xxx, 5, "NAME", Buffer[256])
// prctl(xxx, 32, "NAME", Buffer[256])
// success return +out, error return -N
#define SUKISU_KPM_INFO 32
// prctl(xxx, xxx, 6, "NAME", "ARGS")
// prctl(xxx, 33, "NAME", "ARGS")
// success return KPM's result value
// error return -N
#define SUKISU_KPM_CONTROL 33
// prctl(xxx, xxx, 7)
// success will printf to stdout and return 0
// error will return -1
#define SUKISU_KPM_PRINT 34
// prctl(xxx, 34, buffer, bufferSize)
// success return KPM's result value
// error return -N
#define SUKISU_KPM_VERSION 34
#endif

View File

@@ -0,0 +1,289 @@
#include <linux/export.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/kernfs.h>
#include <linux/file.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/uaccess.h>
#include <linux/elf.h>
#include <linux/kallsyms.h>
#include <linux/version.h>
#include <linux/list.h>
#include <linux/spinlock.h>
#include <linux/rcupdate.h>
#include <asm/elf.h> /* 包含 ARM64 重定位类型定义 */
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <linux/string.h>
#include <asm/cacheflush.h>
#include <linux/module.h>
#include <linux/vmalloc.h>
#include <linux/set_memory.h>
#include <linux/version.h>
#include <linux/export.h>
#include <linux/slab.h>
#include "kpm.h"
#include "compact.h"
#include <linux/types.h>
#include <linux/stddef.h>
// 结构体成员元数据
struct DynamicStructMember {
const char* name;
size_t size;
size_t offset;
};
// 结构体元数据(包含总大小)
struct DynamicStructInfo {
const char* name;
size_t count;
size_t total_size;
struct DynamicStructMember* members;
};
// 定义结构体元数据的宏(直接使用 struct 名称)
#define DYNAMIC_STRUCT_BEGIN(struct_name) \
static struct DynamicStructMember struct_name##_members[] = {
#define DEFINE_MEMBER(struct_name, member) \
{ \
.name = #member, \
.size = sizeof(((struct struct_name*)0)->member), \
.offset = offsetof(struct struct_name, member) \
},
#define DYNAMIC_STRUCT_END(struct_name) \
}; \
static struct DynamicStructInfo struct_name##_info = { \
.name = #struct_name, \
.count = sizeof(struct_name##_members) / sizeof(struct DynamicStructMember), \
.total_size = sizeof(struct struct_name), \
.members = struct_name##_members \
};
// ==================================================================================
#include <linux/version.h>
#define KERNEL_VERSION_6_1 KERNEL_VERSION(6, 1, 0)
#define KERNEL_VERSION_5_15 KERNEL_VERSION(5, 15, 0)
#include <../fs/mount.h>
#include <linux/mount.h>
// 定义元数据
DYNAMIC_STRUCT_BEGIN(mount)
DEFINE_MEMBER(mount, mnt_parent)
DEFINE_MEMBER(mount, mnt)
DEFINE_MEMBER(mount, mnt_id)
DEFINE_MEMBER(mount, mnt_group_id)
DEFINE_MEMBER(mount, mnt_expiry_mark)
DEFINE_MEMBER(mount, mnt_master)
DEFINE_MEMBER(mount, mnt_devname)
DYNAMIC_STRUCT_END(mount)
DYNAMIC_STRUCT_BEGIN(vfsmount)
DEFINE_MEMBER(vfsmount, mnt_root)
DEFINE_MEMBER(vfsmount, mnt_sb)
DEFINE_MEMBER(vfsmount, mnt_flags)
DYNAMIC_STRUCT_END(vfsmount)
DYNAMIC_STRUCT_BEGIN(mnt_namespace)
DEFINE_MEMBER(mnt_namespace, ns)
DEFINE_MEMBER(mnt_namespace, root)
DEFINE_MEMBER(mnt_namespace, seq)
DEFINE_MEMBER(mnt_namespace, mounts)
#if LINUX_VERSION_CODE < KERNEL_VERSION_5_15
DEFINE_MEMBER(mnt_namespace, count)
#endif
DYNAMIC_STRUCT_END(mnt_namespace)
#include <linux/kprobes.h>
#ifdef CONFIG_KPROBES
DYNAMIC_STRUCT_BEGIN(kprobe)
DEFINE_MEMBER(kprobe, addr)
DEFINE_MEMBER(kprobe, symbol_name)
DEFINE_MEMBER(kprobe, offset)
DEFINE_MEMBER(kprobe, pre_handler)
DEFINE_MEMBER(kprobe, post_handler)
#if LINUX_VERSION_CODE < KERNEL_VERSION_5_15
DEFINE_MEMBER(kprobe, fault_handler)
#endif
DEFINE_MEMBER(kprobe, flags)
DYNAMIC_STRUCT_END(kprobe)
#endif
#include <linux/mm.h>
#include <linux/mm_types.h>
DYNAMIC_STRUCT_BEGIN(vm_area_struct)
DEFINE_MEMBER(vm_area_struct,vm_start)
DEFINE_MEMBER(vm_area_struct,vm_end)
DEFINE_MEMBER(vm_area_struct,vm_flags)
DEFINE_MEMBER(vm_area_struct,anon_vma)
DEFINE_MEMBER(vm_area_struct,vm_pgoff)
DEFINE_MEMBER(vm_area_struct,vm_file)
DEFINE_MEMBER(vm_area_struct,vm_private_data)
#ifdef CONFIG_ANON_VMA_NAME
DEFINE_MEMBER(vm_area_struct, anon_name)
#endif
DEFINE_MEMBER(vm_area_struct, vm_ops)
DYNAMIC_STRUCT_END(vm_area_struct)
DYNAMIC_STRUCT_BEGIN(vm_operations_struct)
DEFINE_MEMBER(vm_operations_struct, open)
DEFINE_MEMBER(vm_operations_struct, close)
DEFINE_MEMBER(vm_operations_struct, name)
DEFINE_MEMBER(vm_operations_struct, access)
DYNAMIC_STRUCT_END(vm_operations_struct)
#include <linux/netlink.h>
DYNAMIC_STRUCT_BEGIN(netlink_kernel_cfg)
DEFINE_MEMBER(netlink_kernel_cfg, groups)
DEFINE_MEMBER(netlink_kernel_cfg, flags)
DEFINE_MEMBER(netlink_kernel_cfg, input)
DEFINE_MEMBER(netlink_kernel_cfg, cb_mutex)
DEFINE_MEMBER(netlink_kernel_cfg, bind)
DEFINE_MEMBER(netlink_kernel_cfg, unbind)
#if LINUX_VERSION_CODE < KERNEL_VERSION_6_1
DEFINE_MEMBER(netlink_kernel_cfg, compare)
#endif
DYNAMIC_STRUCT_END(netlink_kernel_cfg)
#include <linux/sched.h>
DYNAMIC_STRUCT_BEGIN(task_struct)
DEFINE_MEMBER(task_struct, pid)
DEFINE_MEMBER(task_struct, tgid)
DEFINE_MEMBER(task_struct, cred)
DEFINE_MEMBER(task_struct, real_cred)
DEFINE_MEMBER(task_struct, comm)
DEFINE_MEMBER(task_struct, parent)
DEFINE_MEMBER(task_struct, group_leader)
DEFINE_MEMBER(task_struct, mm)
DEFINE_MEMBER(task_struct, active_mm)
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
DEFINE_MEMBER(task_struct, pids[PIDTYPE_PID].pid)
#else
DEFINE_MEMBER(task_struct, thread_pid)
#endif
DEFINE_MEMBER(task_struct, files)
DEFINE_MEMBER(task_struct, seccomp)
#ifdef CONFIG_THREAD_INFO_IN_TASK
DEFINE_MEMBER(task_struct, thread_info)
#endif
#ifdef CONFIG_CGROUPS
DEFINE_MEMBER(task_struct, cgroups)
#endif
#ifdef CONFIG_SECURITY
DEFINE_MEMBER(task_struct, security)
#endif
DEFINE_MEMBER(task_struct, thread)
DYNAMIC_STRUCT_END(task_struct)
// =====================================================================================================================
#define STRUCT_INFO(name) &(name##_info)
static
struct DynamicStructInfo* dynamic_struct_infos[] = {
STRUCT_INFO(mount),
STRUCT_INFO(vfsmount),
STRUCT_INFO(mnt_namespace),
#ifdef CONFIG_KPROBES
STRUCT_INFO(kprobe),
#endif
STRUCT_INFO(vm_area_struct),
STRUCT_INFO(vm_operations_struct),
STRUCT_INFO(netlink_kernel_cfg),
STRUCT_INFO(task_struct)
};
// return 0 if successful
// return -1 if struct not defined
int sukisu_super_find_struct(
const char* struct_name,
size_t* out_size,
int* out_members
) {
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
struct DynamicStructInfo* info = dynamic_struct_infos[i];
if(strcmp(struct_name, info->name) == 0) {
if(out_size)
*out_size = info->total_size;
if(out_members)
*out_members = info->count;
return 0;
}
}
return -1;
}
EXPORT_SYMBOL(sukisu_super_find_struct);
// Dynamic access struct
// return 0 if successful
// return -1 if struct not defined
// return -2 if member not defined
int sukisu_super_access (
const char* struct_name,
const char* member_name,
size_t* out_offset,
size_t* out_size
) {
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
struct DynamicStructInfo* info = dynamic_struct_infos[i];
if(strcmp(struct_name, info->name) == 0) {
for (size_t i1 = 0; i1 < info->count; i1++) {
if (strcmp(info->members[i1].name, member_name) == 0) {
if(out_offset)
*out_offset = info->members[i].offset;
if(out_size)
*out_size = info->members[i].size;
return 0;
}
}
return -2;
}
}
return -1;
}
EXPORT_SYMBOL(sukisu_super_access);
// 动态 container_of 宏
#define DYNAMIC_CONTAINER_OF(offset, member_ptr) ({ \
(offset != (size_t)-1) ? (void*)((char*)(member_ptr) - offset) : NULL; \
})
// Dynamic container_of
// return 0 if success
// return -1 if current struct not defined
// return -2 if target member not defined
int sukisu_super_container_of(
const char* struct_name,
const char* member_name,
void* ptr,
void** out_ptr
) {
if(ptr == NULL) {
return -3;
}
for(size_t i = 0; i < (sizeof(dynamic_struct_infos) / sizeof(dynamic_struct_infos[0])); i++) {
struct DynamicStructInfo* info = dynamic_struct_infos[i];
if(strcmp(struct_name, info->name) == 0) {
for (size_t i1 = 0; i1 < info->count; i1++) {
if (strcmp(info->members[i1].name, member_name) == 0) {
*out_ptr = (void*) DYNAMIC_CONTAINER_OF(info->members[i1].offset, ptr);
return 0;
}
}
return -2;
}
}
return -1;
}
EXPORT_SYMBOL(sukisu_super_container_of);

View File

@@ -0,0 +1,39 @@
#ifndef __SUKISU_SUPER_ACCESS_H
#define __SUKISU_SUPER_ACCESS_H
#include <linux/types.h>
#include <linux/stddef.h>
#include "kpm.h"
#include "compact.h"
// return 0 if successful
// return -1 if struct not defined
int sukisu_super_find_struct(
const char* struct_name,
size_t* out_size,
int* out_members
);
// Dynamic access struct
// return 0 if successful
// return -1 if struct not defined
// return -2 if member not defined
int sukisu_super_access (
const char* struct_name,
const char* member_name,
size_t* out_offset,
size_t* out_size
);
// Dynamic container_of
// return 0 if success
// return -1 if current struct not defined
// return -2 if target member not defined
int sukisu_super_container_of(
const char* struct_name,
const char* member_name,
void* ptr,
void** out_ptr
);
#endif

View File

@@ -36,6 +36,10 @@ extern void ksu_sucompat_init();
extern void ksu_sucompat_exit();
extern void ksu_ksud_init();
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)
{
@@ -56,13 +60,17 @@ int __init kernelsu_init(void)
ksu_allowlist_init();
ksu_throne_tracker_init();
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
ksu_sucompat_init();
ksu_ksud_init();
#else
pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html");
#endif
#ifdef CONFIG_KSU_TRACEPOINT_HOOK
ksu_trace_register();
#endif
#ifdef MODULE
#ifndef CONFIG_KSU_DEBUG
kobject_del(&THIS_MODULE->mkobj.kobj);
@@ -79,11 +87,15 @@ void kernelsu_exit(void)
destroy_workqueue(ksu_workqueue);
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
ksu_ksud_exit();
ksu_sucompat_exit();
#endif
#ifdef CONFIG_KSU_TRACEPOINT_HOOK
ksu_trace_unregister();
#endif
ksu_core_exit();
}

View File

@@ -24,6 +24,13 @@
#define CMD_IS_SU_ENABLED 14
#define CMD_ENABLE_SU 15
#define CMD_GET_FULL_VERSION 0xC0FFEE1A
#define CMD_ENABLE_KPM 100
#define CMD_HOOK_TYPE 101
#define CMD_DYNAMIC_SIGN 103
#define CMD_GET_MANAGERS 104
#define EVENT_POST_FS_DATA 1
#define EVENT_BOOT_COMPLETED 2
#define EVENT_MODULE_MOUNTED 3
@@ -34,6 +41,30 @@
#define KSU_MAX_GROUPS 32
#define KSU_SELINUX_DOMAIN 64
// SukiSU Ultra kernel su version full strings
#ifndef KSU_VERSION_FULL
#define KSU_VERSION_FULL "v3.x-00000000@unknown"
#endif
#define KSU_FULL_VERSION_STRING 255
#define DYNAMIC_SIGN_OP_SET 0
#define DYNAMIC_SIGN_OP_GET 1
#define DYNAMIC_SIGN_OP_CLEAR 2
struct dynamic_sign_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 {
int32_t uid;
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_input_hook();
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
static struct work_struct stop_vfs_read_work;
static struct work_struct stop_execve_hook_work;
static struct work_struct stop_input_hook_work;
@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
// Detect whether it is on or not
static bool is_boot_phase = true;
#ifdef CONFIG_COMPAT
bool ksu_is_compat __read_mostly = false;
#endif
void on_post_fs_data(void)
{
static bool done = false;
@@ -107,6 +111,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
if (get_user(compat, argv.ptr.compat + nr))
return ERR_PTR(-EFAULT);
ksu_is_compat = true;
return compat_ptr(compat);
}
#endif
@@ -157,7 +162,7 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,
struct user_arg_ptr *argv,
struct user_arg_ptr *envp, int *flags)
{
#ifndef CONFIG_KPROBES
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_execveat_hook) {
return 0;
}
@@ -313,7 +318,7 @@ static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to)
int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,
size_t *count_ptr, loff_t **pos)
{
#ifndef CONFIG_KPROBES
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_vfs_read_hook) {
return 0;
}
@@ -426,7 +431,7 @@ static bool is_volumedown_enough(unsigned int count)
int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,
int *value)
{
#ifndef CONFIG_KPROBES
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_input_hook) {
return 0;
}
@@ -468,7 +473,7 @@ bool ksu_is_safe_mode()
return false;
}
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
struct pt_regs *real_regs = PT_REAL_REGS(regs);
@@ -544,7 +549,7 @@ static void do_stop_input_hook(struct work_struct *work)
static void stop_vfs_read_hook()
{
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
bool ret = schedule_work(&stop_vfs_read_work);
pr_info("unregister vfs_read kprobe: %d!\n", ret);
#else
@@ -555,7 +560,7 @@ static void stop_vfs_read_hook()
static void stop_execve_hook()
{
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
bool ret = schedule_work(&stop_execve_hook_work);
pr_info("unregister execve kprobe: %d!\n", ret);
#else
@@ -571,7 +576,7 @@ static void stop_input_hook()
return;
}
input_hook_stopped = true;
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
bool ret = schedule_work(&stop_input_hook_work);
pr_info("unregister input kprobe: %d!\n", ret);
#else
@@ -583,7 +588,7 @@ static void stop_input_hook()
// ksud: module support
void ksu_ksud_init()
{
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
int ret;
ret = register_kprobe(&execve_kp);
@@ -603,12 +608,12 @@ void ksu_ksud_init()
void ksu_ksud_exit()
{
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
unregister_kprobe(&execve_kp);
// this should be done before unregister vfs_read_kp
// unregister_kprobe(&vfs_read_kp);
unregister_kprobe(&input_event_kp);
#endif
is_boot_phase = false;
#endif
}

View File

@@ -8,6 +8,11 @@
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()
{
return ksu_manager_uid != KSU_INVALID_UID;
@@ -15,7 +20,7 @@ static inline bool ksu_is_manager_uid_valid()
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()

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)
{
struct policydb *db;
struct selinux_policy *policy = rcu_dereference(selinux_state.policy);
struct selinux_policy *policy = selinux_state.policy;
db = &policy->policydb;
return db;
}
static DEFINE_MUTEX(ksu_rules);
void apply_kernelsu_rules()
{
struct policydb *db;
if (!getenforce()) {
pr_info("SELinux permissive or disabled, apply rules!\n");
}
rcu_read_lock();
struct policydb *db = get_policydb();
mutex_lock(&ksu_rules);
db = get_policydb();
ksu_permissive(db, KERNEL_SU_DOMAIN);
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", "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
@@ -137,17 +145,45 @@ void apply_kernelsu_rules()
#define CMD_TYPE_CHANGE 8
#define CMD_GENFSCON 9
#ifdef CONFIG_64BIT
struct sepol_data {
u32 cmd;
u32 subcmd;
char __user *sepol1;
char __user *sepol2;
char __user *sepol3;
char __user *sepol4;
char __user *sepol5;
char __user *sepol6;
char __user *sepol7;
u64 field_sepol1;
u64 field_sepol2;
u64 field_sepol3;
u64 field_sepol4;
u64 field_sepol5;
u64 field_sepol6;
u64 field_sepol7;
};
#ifdef CONFIG_COMPAT
extern bool ksu_is_compat __read_mostly;
struct sepol_compat_data {
u32 cmd;
u32 subcmd;
u32 field_sepol1;
u32 field_sepol2;
u32 field_sepol3;
u32 field_sepol4;
u32 field_sepol5;
u32 field_sepol6;
u32 field_sepol7;
};
#endif // CONFIG_COMPAT
#else
struct sepol_data {
u32 cmd;
u32 subcmd;
u32 field_sepol1;
u32 field_sepol2;
u32 field_sepol3;
u32 field_sepol4;
u32 field_sepol5;
u32 field_sepol6;
u32 field_sepol7;
};
#endif // CONFIG_64BIT
static int get_object(char *buf, char __user *user_object, size_t buf_sz,
char **object)
@@ -184,6 +220,8 @@ static void reset_avc_cache()
int handle_sepolicy(unsigned long arg3, void __user *arg4)
{
struct policydb *db;
if (!arg4) {
return -1;
}
@@ -192,18 +230,62 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
pr_info("SELinux permissive or disabled when handle policy!\n");
}
u32 cmd, subcmd;
char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7;
#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT)
if (unlikely(ksu_is_compat)) {
struct sepol_compat_data compat_data;
if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) {
pr_err("sepol: copy sepol_data failed.\n");
return -1;
}
sepol1 = compat_ptr(compat_data.field_sepol1);
sepol2 = compat_ptr(compat_data.field_sepol2);
sepol3 = compat_ptr(compat_data.field_sepol3);
sepol4 = compat_ptr(compat_data.field_sepol4);
sepol5 = compat_ptr(compat_data.field_sepol5);
sepol6 = compat_ptr(compat_data.field_sepol6);
sepol7 = compat_ptr(compat_data.field_sepol7);
cmd = compat_data.cmd;
subcmd = compat_data.subcmd;
} else {
struct sepol_data data;
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
pr_err("sepol: copy sepol_data failed.\n");
return -1;
}
sepol1 = data.field_sepol1;
sepol2 = data.field_sepol2;
sepol3 = data.field_sepol3;
sepol4 = data.field_sepol4;
sepol5 = data.field_sepol5;
sepol6 = data.field_sepol6;
sepol7 = data.field_sepol7;
cmd = data.cmd;
subcmd = data.subcmd;
}
#else
// basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n)
struct sepol_data data;
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
pr_err("sepol: copy sepol_data failed.\n");
return -1;
}
sepol1 = data.field_sepol1;
sepol2 = data.field_sepol2;
sepol3 = data.field_sepol3;
sepol4 = data.field_sepol4;
sepol5 = data.field_sepol5;
sepol6 = data.field_sepol6;
sepol7 = data.field_sepol7;
cmd = data.cmd;
subcmd = data.subcmd;
#endif
u32 cmd = data.cmd;
u32 subcmd = data.subcmd;
mutex_lock(&ksu_rules);
rcu_read_lock();
struct policydb *db = get_policydb();
db = get_policydb();
int ret = -1;
if (cmd == CMD_NORMAL_PERM) {
@@ -213,22 +295,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char perm_buf[MAX_SEPOL_LEN];
char *s, *t, *c, *p;
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
pr_err("sepol: copy src failed.\n");
goto exit;
}
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
pr_err("sepol: copy tgt failed.\n");
goto exit;
}
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
pr_err("sepol: copy cls failed.\n");
goto exit;
}
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) <
if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
0) {
pr_err("sepol: copy perm failed.\n");
goto exit;
@@ -258,24 +340,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char perm_set[MAX_SEPOL_LEN];
char *s, *t, *c;
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) {
if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
pr_err("sepol: copy src failed.\n");
goto exit;
}
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) {
if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
pr_err("sepol: copy tgt failed.\n");
goto exit;
}
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) {
if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
pr_err("sepol: copy cls failed.\n");
goto exit;
}
if (strncpy_from_user(operation, data.sepol4,
if (strncpy_from_user(operation, sepol4,
sizeof(operation)) < 0) {
pr_err("sepol: copy operation failed.\n");
goto exit;
}
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) <
if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
0) {
pr_err("sepol: copy perm_set failed.\n");
goto exit;
@@ -295,7 +377,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
} else if (cmd == CMD_TYPE_STATE) {
char src[MAX_SEPOL_LEN];
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
pr_err("sepol: copy src failed.\n");
goto exit;
}
@@ -315,11 +397,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char type[MAX_SEPOL_LEN];
char attr[MAX_SEPOL_LEN];
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) {
if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) {
pr_err("sepol: copy type failed.\n");
goto exit;
}
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) {
if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) {
pr_err("sepol: copy attr failed.\n");
goto exit;
}
@@ -339,7 +421,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
} else if (cmd == CMD_ATTR) {
char attr[MAX_SEPOL_LEN];
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) {
if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) {
pr_err("sepol: copy attr failed.\n");
goto exit;
}
@@ -356,28 +438,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char default_type[MAX_SEPOL_LEN];
char object[MAX_SEPOL_LEN];
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
pr_err("sepol: copy src failed.\n");
goto exit;
}
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
pr_err("sepol: copy tgt failed.\n");
goto exit;
}
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
pr_err("sepol: copy cls failed.\n");
goto exit;
}
if (strncpy_from_user(default_type, data.sepol4,
if (strncpy_from_user(default_type, sepol4,
sizeof(default_type)) < 0) {
pr_err("sepol: copy default_type failed.\n");
goto exit;
}
char *real_object;
if (data.sepol5 == NULL) {
if (sepol5 == NULL) {
real_object = NULL;
} else {
if (strncpy_from_user(object, data.sepol5,
if (strncpy_from_user(object, sepol5,
sizeof(object)) < 0) {
pr_err("sepol: copy object failed.\n");
goto exit;
@@ -396,19 +478,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char cls[MAX_SEPOL_LEN];
char default_type[MAX_SEPOL_LEN];
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) {
if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
pr_err("sepol: copy src failed.\n");
goto exit;
}
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) {
if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
pr_err("sepol: copy tgt failed.\n");
goto exit;
}
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) {
if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
pr_err("sepol: copy cls failed.\n");
goto exit;
}
if (strncpy_from_user(default_type, data.sepol4,
if (strncpy_from_user(default_type, sepol4,
sizeof(default_type)) < 0) {
pr_err("sepol: copy default_type failed.\n");
goto exit;
@@ -429,15 +511,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char name[MAX_SEPOL_LEN];
char path[MAX_SEPOL_LEN];
char context[MAX_SEPOL_LEN];
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) {
if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) {
pr_err("sepol: copy name failed.\n");
goto exit;
}
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) {
if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) {
pr_err("sepol: copy path failed.\n");
goto exit;
}
if (strncpy_from_user(context, data.sepol3, sizeof(context)) <
if (strncpy_from_user(context, sepol3, sizeof(context)) <
0) {
pr_err("sepol: copy context failed.\n");
goto exit;
@@ -453,7 +535,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
}
exit:
rcu_read_unlock();
mutex_unlock(&ksu_rules);
// 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.

View File

@@ -1,7 +1,7 @@
#!/bin/sh
set -eu
GKI_ROOT=$(pwd)
KERNEL_ROOT=$(pwd)
display_usage() {
echo "Usage: $0 [--cleanup | <commit-or-tag>]"
@@ -12,10 +12,10 @@ display_usage() {
}
initialize_variables() {
if test -d "$GKI_ROOT/common/drivers"; then
DRIVER_DIR="$GKI_ROOT/common/drivers"
elif test -d "$GKI_ROOT/drivers"; then
DRIVER_DIR="$GKI_ROOT/drivers"
if test -d "$KERNEL_ROOT/common/drivers"; then
DRIVER_DIR="$KERNEL_ROOT/common/drivers"
elif test -d "$KERNEL_ROOT/drivers"; then
DRIVER_DIR="$KERNEL_ROOT/drivers"
else
echo '[ERROR] "drivers/" directory not found.'
exit 127
@@ -30,22 +30,21 @@ perform_cleanup() {
echo "[+] Cleaning up..."
[ -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 "drivers/kernelsu/Kconfig" "$DRIVER_KCONFIG" && sed -i '/drivers\/kernelsu\/Kconfig/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted."
if [ -d "$GKI_ROOT/KernelSU" ]; then
rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted."
grep -q "kernelsu" "$DRIVER_KCONFIG" && sed -i '/kernelsu/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted."
if [ -d "$KERNEL_ROOT/KernelSU" ]; then
rm -rf "$KERNEL_ROOT/KernelSU" && echo "[-] KernelSU directory deleted."
fi
}
# Sets up or update KernelSU environment
setup_kernelsu() {
echo "[+] Setting up KernelSU..."
# Clone the repository and rename it to KernelSU
if [ ! -d "$GKI_ROOT/KernelSU" ]; then
git clone https://github.com/ShirkNeko/SukiSU-Ultra SukiSU-Ultra
mv SukiSU-Ultra KernelSU
echo "[+] Repository cloned and renamed to KernelSU."
# Clone the repository
if [ ! -d "$KERNEL_ROOT/KernelSU" ]; then
git clone https://github.com/SukiSU-Ultra/SukiSU-Ultra KernelSU
echo "[+] Repository cloned."
fi
cd "$GKI_ROOT/KernelSU"
cd "$KERNEL_ROOT/KernelSU"
git stash && echo "[-] Stashed current changes."
if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then
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"
fi
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
grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$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 "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."
echo '[+] Done.'
}

View File

@@ -22,6 +22,10 @@
extern void escape_to_root();
#ifndef CONFIG_KSU_KPROBES_HOOK
static bool ksu_sucompat_non_kp __read_mostly = true;
#endif
static void __user *userspace_stack_buffer(const void *d, size_t len)
{
/* To avoid having to mmap a page in userspace, just write below the stack
@@ -50,6 +54,12 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
{
const char su[] = SU_PATH;
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp) {
return 0;
}
#endif
if (!ksu_is_allow_uid(current_uid().val)) {
return 0;
}
@@ -71,6 +81,11 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
// const char sh[] = SH_PATH;
const char su[] = SU_PATH;
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp) {
return 0;
}
#endif
if (!ksu_is_allow_uid(current_uid().val)) {
return 0;
}
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
const char sh[] = KSUD_PATH;
const char su[] = SU_PATH;
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp) {
return 0;
}
#endif
if (unlikely(!filename_ptr))
return 0;
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
const char su[] = SU_PATH;
char path[sizeof(su) + 1];
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_sucompat_non_kp){
return 0;
}
#endif
if (unlikely(!filename_user))
return 0;
@@ -164,7 +189,48 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
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)
{
struct pt_regs *real_regs = PT_REAL_REGS(regs);
@@ -197,6 +263,24 @@ static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
NULL);
}
#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,
kprobe_pre_handler_t handler)
{
@@ -227,24 +311,32 @@ static void destroy_kprobe(struct kprobe **kp_ptr)
*kp_ptr = NULL;
}
static struct kprobe *su_kps[3];
#endif
// sucompat: permited process can execute 'su' to gain root access.
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[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
#ifdef MODULE
su_kps[3] = init_kprobe("pts_unix98_lookup", pts_unix98_lookup_pre);
#endif
#else
ksu_sucompat_non_kp = true;
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
#endif
}
void ksu_sucompat_exit()
{
#ifdef CONFIG_KPROBES
#ifdef CONFIG_KSU_KPROBES_HOOK
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
destroy_kprobe(&su_kps[i]);
}
#else
ksu_sucompat_non_kp = false;
pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n");
#endif
}

View File

@@ -12,6 +12,7 @@
#include "manager.h"
#include "throne_tracker.h"
#include "kernel_compat.h"
#include "dynamic_sign.h"
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;
}
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];
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;
}
pr_info("manager pkg: %s\n", pkg);
pr_info("manager pkg: %s, signature_index: %d\n", pkg, signature_index);
#ifdef KSU_MANAGER_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) {
if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) {
pr_info("Crowning manager: %s(uid=%d)\n", pkg, np->uid);
pr_info("Crowning manager: %s(uid=%d, signature_index=%d)\n", pkg, np->uid, signature_index);
if (signature_index == 1 || signature_index == 2) {
ksu_add_manager(np->uid, signature_index);
if (!ksu_is_manager_uid_valid()) {
ksu_set_manager_uid(np->uid);
}
} else {
ksu_set_manager_uid(np->uid);
}
break;
}
}
@@ -185,11 +195,24 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
}
}
bool is_manager = is_manager_apk(dirpath);
pr_info("Found new base.apk at path: %s, is_manager: %d\n",
dirpath, is_manager);
if (is_manager) {
crown_manager(dirpath, my_ctx->private_data);
int signature_index = -1;
bool is_multi_manager = ksu_is_multi_manager_apk(dirpath, &signature_index);
pr_info("Found new base.apk at path: %s, is_multi_manager: %d, signature_index: %d\n",
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;
// Manager found, clear APK cache list
@@ -199,12 +222,14 @@ FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,
}
} else {
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);
}
}
}
}
return FILLDIR_ACTOR_CONTINUE;
}
@@ -214,6 +239,7 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
int i, stop = 0;
struct list_head data_path_list;
INIT_LIST_HEAD(&data_path_list);
unsigned long data_app_magic = 0;
// Initialize APK cache list
struct apk_path_hash *pos, *n;
@@ -246,6 +272,24 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
goto skip_iterate;
}
// grab magic on first folder, which is /data/app
if (!data_app_magic) {
if (file->f_inode->i_sb->s_magic) {
data_app_magic = file->f_inode->i_sb->s_magic;
pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, pos->dirpath, data_app_magic);
} else {
filp_close(file, NULL);
goto skip_iterate;
}
}
if (file->f_inode->i_sb->s_magic != data_app_magic) {
pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", __func__, pos->dirpath,
file->f_inode->i_sb->s_magic, data_app_magic);
filp_close(file, NULL);
goto skip_iterate;
}
iterate_dir(file, &ctx.ctx);
filp_close(file, NULL);
}
@@ -344,6 +388,8 @@ void track_throne()
// first, check if manager_uid exist!
bool manager_exist = false;
bool dynamic_manager_exist = false;
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
// don't delete it in this case!
@@ -354,16 +400,33 @@ void track_throne()
}
}
// Check for dynamic managers
if (!dynamic_manager_exist && ksu_is_dynamic_sign_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 (ksu_is_manager_uid_valid()) {
pr_info("manager is uninstalled, invalidate it!\n");
ksu_invalidate_manager_uid();
goto prune;
}
pr_info("Searching manager...\n");
search_manager("/data/app", 2, &uid_list);
pr_info("Search manager finished\n");
} else if (!dynamic_manager_exist && ksu_is_dynamic_sign_enabled()) {
// Always perform search when called from dynamic sign rescan
pr_info("Dynamic sign enabled, Searching manager...\n");
search_manager("/data/app", 2, &uid_list);
pr_info("Search Dynamic sign manager finished\n");
}
prune:
// then prune the allowlist
ksu_prune_allowlist(is_uid_exist, &uid_list);
out:

View File

@@ -24,19 +24,34 @@ apksign {
keyPasswordProperty = "KEY_PASSWORD"
}
android {
namespace = "shirkneko.zako.sukisu"
/**signingConfigs {
create("Debug") {
storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore")
storePassword = ""
keyAlias = ""
keyPassword = ""
}
}**/
namespace = "com.sukisu.ultra"
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
/**debug {
signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig
}**/
}
buildFeatures {
aidl = true
buildConfig = true
compose = true
prefab = true
@@ -94,7 +109,12 @@ android {
}
}
ksp {
arg("compose-destinations.defaultTransitions", "none")
}
dependencies {
implementation(libs.gson)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.navigation.compose)
@@ -114,6 +134,8 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.swiperefreshlayout)
implementation(libs.compose.destinations.core)
ksp(libs.compose.destinations.ksp)
@@ -140,4 +162,20 @@ dependencies {
implementation(libs.com.github.topjohnwu.libsu.core)
implementation(libs.mmrl.platform)
compileOnly(libs.mmrl.hidden.api)
/**
* Compile only `Java-Native-Access` since plugins are disabled in both WebUI X and KSU WebUI
* Avoid using:
* - fun WXInterface.registerLibrary(clazz: Class<*>, name: String): Unit
* - fun WXInterface.unregisterLibrary(clazz: Class<*>): Unit
* - fun WXInterface.isLibraryRegistered(libName: String): Boolean
*/
compileOnly(libs.mmrl.webui.jna)
implementation(libs.mmrl.webui.portable)
implementation(libs.mmrl.ui)
implementation(libs.accompanist.drawablepainter)
}

View File

@@ -0,0 +1,18 @@
-verbose
-optimizationpasses 5
-dontwarn org.conscrypt.**
-dontwarn kotlinx.serialization.**
# MMRL:webui reflection
-keep class androidx.compose.ui.graphics.Color { *; }
-keep class androidx.compose.material3.ButtonColors { *; }
-keep class androidx.compose.material3.CardColors { *; }
-keep class androidx.compose.material3.ColorScheme { *; }
-keep class com.dergoogler.mmrl.platform.model.ModId { *; }
-keep class com.dergoogler.mmrl.webui.interfaces.WXOptions { *; }
-keep class com.dergoogler.mmrl.webui.interfaces.WXInterface { *; }
-keep class com.dergoogler.mmrl.webui.interfaces.** { *; }
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }

View File

@@ -3,8 +3,10 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<application
@@ -30,6 +32,19 @@
</intent-filter>
</activity>
<activity-alias
android:name=".ui.MainActivityAlias"
android:exported="true"
android:enabled="false"
android:icon="@mipmap/ic_launcher_alt"
android:roundIcon="@mipmap/ic_launcher_alt_round"
android:targetActivity=".ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity
android:name=".ui.webui.WebUIActivity"
android:autoRemoveFromRecents="true"
@@ -37,6 +52,13 @@
android:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" />
<activity
android:name=".ui.webui.WebUIXActivity"
android:autoRemoveFromRecents="true"
android:documentLaunchMode="intoExisting"
android:exported="false"
android:theme="@style/Theme.KernelSU.WebUI" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"

View File

@@ -1,9 +0,0 @@
// IKsuInterface.aidl
package shirkneko.zako.sukisu;
import android.content.pm.PackageInfo;
import rikka.parcelablelist.ParcelableListSlice;
interface IKsuInterface {
ParcelableListSlice<PackageInfo> getPackages(int flags);
}

Binary file not shown.

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
# documentation: https://d.android.com/studio/projects/add-native-code.html
@@ -7,15 +6,22 @@ cmake_minimum_required(VERSION 3.18.1)
project("kernelsu")
find_package(cxx REQUIRED CONFIG)
link_libraries(cxx::cxx)
add_library(zako
SHARED
jni.cc
ksu.cc
jni.c
ksu.c
)
find_library(log-lib log)
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 sign
NativeBridge(setDynamicSign, jboolean, jint size, jstring hash) {
if (!hash) {
LogDebug("setDynamicSign: hash is null");
return false;
}
const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr);
bool result = set_dynamic_sign((unsigned int)size, chash);
GetEnvironment()->ReleaseStringUTFChars(env, hash, chash);
LogDebug("setDynamicSign: size=0x%x, result=%d", size, result);
return result;
}
NativeBridgeNP(getDynamicSign, jobject) {
struct dynamic_sign_user_config config;
bool result = get_dynamic_sign(&config);
if (!result) {
LogDebug("getDynamicSign: failed to get dynamic sign config");
return NULL;
}
jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicSignConfig");
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicSignConfig");
SET_INT_FIELD(obj, cls, size, (jint)config.size);
SET_STRING_FIELD(obj, cls, hash, config.hash);
LogDebug("getDynamicSign: size=0x%x, hash=%.16s...", config.size, config.hash);
return obj;
}
NativeBridgeNP(clearDynamicSign, jboolean) {
bool result = clear_dynamic_sign();
LogDebug("clearDynamicSign: 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,308 +0,0 @@
#include <jni.h>
#include <sys/prctl.h>
#include <android/log.h>
#include <cstring>
#include "ksu.h"
#define LOG_TAG "KernelSU"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
extern "C"
JNIEXPORT jboolean JNICALL
Java_shirkneko_zako_sukisu_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_shirkneko_zako_sukisu_Natives_getVersion(JNIEnv *env, jobject) {
return get_version();
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_shirkneko_zako_sukisu_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_shirkneko_zako_sukisu_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
return is_safe_mode();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_shirkneko_zako_sukisu_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_shirkneko_zako_sukisu_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("shirkneko/zako/sukisu/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_shirkneko_zako_sukisu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
auto cls = env->FindClass("shirkneko/zako/sukisu/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_shirkneko_zako_sukisu_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
return uid_should_umount(uid);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_shirkneko_zako_sukisu_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
return is_su_enabled();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_shirkneko_zako_sukisu_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
return set_su_enabled(enabled);
}

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_SIGN 103
#define CMD_GET_MANAGERS 104
#define DYNAMIC_SIGN_OP_SET 0
#define DYNAMIC_SIGN_OP_GET 1
#define DYNAMIC_SIGN_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_sign(unsigned int size, const char* hash) {
if (hash == NULL) {
return false;
}
struct dynamic_sign_user_config config;
config.operation = DYNAMIC_SIGN_OP_SET;
config.size = size;
strncpy(config.hash, hash, sizeof(config.hash) - 1);
config.hash[sizeof(config.hash) - 1] = '\0';
return ksuctl(CMD_DYNAMIC_SIGN, &config, NULL);
}
bool get_dynamic_sign(struct dynamic_sign_user_config* config) {
if (config == NULL) {
return false;
}
config->operation = DYNAMIC_SIGN_OP_GET;
return ksuctl(CMD_DYNAMIC_SIGN, config, NULL);
}
bool clear_dynamic_sign() {
struct dynamic_sign_user_config config;
config.operation = DYNAMIC_SIGN_OP_CLEAR;
return ksuctl(CMD_DYNAMIC_SIGN, &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,99 +0,0 @@
//
// Created by weishu on 2022/12/9.
//
#include <sys/prctl.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include "ksu.h"
#define KERNEL_SU_OPTION 0xDEADBEEF
#define CMD_GRANT_ROOT 0
#define CMD_BECOME_MANAGER 1
#define CMD_GET_VERSION 2
#define CMD_ALLOW_SU 3
#define CMD_DENY_SU 4
#define CMD_GET_SU_LIST 5
#define CMD_GET_DENY_LIST 6
#define CMD_CHECK_SAFEMODE 9
#define CMD_GET_APP_PROFILE 10
#define CMD_SET_APP_PROFILE 11
#define CMD_IS_UID_GRANTED_ROOT 12
#define CMD_IS_UID_SHOULD_UMOUNT 13
#define CMD_IS_SU_ENABLED 14
#define CMD_ENABLE_SU 15
static bool ksuctl(int cmd, void* arg1, void* arg2) {
int32_t result = 0;
prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
return result == KERNEL_SU_OPTION;
}
bool become_manager(const char* pkg) {
char param[128];
uid_t uid = getuid();
uint32_t userId = uid / 100000;
if (userId == 0) {
sprintf(param, "/data/data/%s", pkg);
} else {
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
}
return ksuctl(CMD_BECOME_MANAGER, param, nullptr);
}
// cache the result to avoid unnecessary syscall
static bool is_lkm;
int get_version() {
int32_t version = -1;
int32_t lkm = 0;
ksuctl(CMD_GET_VERSION, &version, &lkm);
if (!is_lkm && lkm != 0) {
is_lkm = true;
}
return version;
}
bool get_allow_list(int *uids, int *size) {
return ksuctl(CMD_GET_SU_LIST, uids, size);
}
bool is_safe_mode() {
return ksuctl(CMD_CHECK_SAFEMODE, nullptr, nullptr);
}
bool is_lkm_mode() {
// you should call get_version first!
return is_lkm;
}
bool uid_should_umount(int uid) {
bool should;
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, reinterpret_cast<void*>(uid), &should) && should;
}
bool set_app_profile(const app_profile *profile) {
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, nullptr);
}
bool get_app_profile(p_key_t key, app_profile *profile) {
return ksuctl(CMD_GET_APP_PROFILE, (void*) profile, nullptr);
}
bool set_su_enabled(bool enabled) {
return ksuctl(CMD_ENABLE_SU, (void*) enabled, nullptr);
}
bool is_su_enabled() {
bool enabled = true;
// if ksuctl failed, we assume su is enabled, and it cannot be disabled.
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr);
return enabled;
}

View File

@@ -5,10 +5,14 @@
#ifndef KERNELSU_KSU_H
#define KERNELSU_KSU_H
#include "prelude.h"
#include <linux/capability.h>
#include <sys/types.h>
bool become_manager(const char *);
void get_full_version(char* buff);
int get_version();
bool get_allow_list(int *uids, int *size);
@@ -25,7 +29,34 @@ bool is_lkm_mode();
#define KSU_MAX_GROUPS 32
#define KSU_SELINUX_DOMAIN 64
using p_key_t = char[KSU_MAX_PACKAGE_NAME];
#define DYNAMIC_SIGN_OP_SET 0
#define DYNAMIC_SIGN_OP_GET 1
#define DYNAMIC_SIGN_OP_CLEAR 2
struct dynamic_sign_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 {
int32_t uid;
@@ -75,12 +106,36 @@ struct app_profile {
};
};
bool set_app_profile(const app_profile *profile);
struct manager_list_info {
int count;
struct {
uid_t uid;
int signature_index;
} managers[2];
};
bool get_app_profile(p_key_t key, app_profile *profile);
bool set_app_profile(const struct app_profile* profile);
bool get_app_profile(char* key, struct app_profile* profile);
bool set_su_enabled(bool enabled);
bool is_su_enabled();
bool is_KPM_enable();
bool get_hook_type(char* hook_type, size_t size);
bool get_susfs_feature_status(struct susfs_feature_status* status);
bool set_dynamic_sign(unsigned int size, const char* hash);
bool get_dynamic_sign(struct dynamic_sign_user_config* config);
bool clear_dynamic_sign();
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -0,0 +1,151 @@
package com.sukisu.ultra
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityOptions
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import coil.Coil
import coil.ImageLoader
import com.dergoogler.mmrl.platform.PlatformManager
import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File
import java.util.Locale
@SuppressLint("StaticFieldLeak")
lateinit var ksuApp: KernelSUApplication
class KernelSUApplication : Application() {
private var currentActivity: Activity? = null
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
currentActivity = activity
}
override fun onActivityStarted(activity: Activity) {
currentActivity = activity
}
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
if (currentActivity == activity) {
currentActivity = null
}
}
}
override fun attachBaseContext(base: Context) {
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var context = base
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(base.resources.configuration)
config.setLocale(locale)
context = base.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
@SuppressLint("ObsoleteSdkInt")
override fun getResources(): Resources {
val resources = super.getResources()
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return createConfigurationContext(config).resources
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
return resources
}
override fun onCreate() {
super.onCreate()
ksuApp = this
// 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
PlatformManager.setHiddenApiExemptions()
val context = this
val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
Coil.setImageLoader(
ImageLoader.Builder(context)
.components {
add(AppIconKeyer())
add(AppIconFetcher.Factory(iconSize, false, context))
}
.build()
)
val webroot = File(dataDir, "webroot")
if (!webroot.exists()) {
webroot.mkdir()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
applyLanguageSetting()
}
@SuppressLint("ObsoleteSdkInt")
private fun applyLanguageSetting() {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
// 添加刷新当前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

@@ -1,4 +1,4 @@
package shirkneko.zako.sukisu
package com.sukisu.ultra
import android.system.Os
@@ -8,24 +8,13 @@ import android.system.Os
*/
data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {
override fun toString(): String {
return "$major.$patchLevel.$subLevel"
}
fun isGKI(): Boolean {
// kernel 6.x
if (major > 5) {
return true
}
// kernel 5.10.x
if (major == 5) {
return patchLevel >= 10
}
return false
override fun toString(): String = "$major.$patchLevel.$subLevel"
fun isGKI(): Boolean = when {
major > 5 -> true
major == 5 && patchLevel >= 10 -> true
else -> false
}
fun isGKI1(): Boolean = (major == 4 && patchLevel >= 19) || (major == 5 && patchLevel < 10)
}
fun parseKernelVersion(version: String): KernelVersion {

View File

@@ -0,0 +1,251 @@
package com.sukisu.ultra
import android.os.Parcelable
import androidx.annotation.Keep
import androidx.compose.runtime.Immutable
import kotlinx.parcelize.Parcelize
/**
* @author weishu
* @date 2022/12/8.
*/
object Natives {
// minimal supported kernel version
// 10915: allowlist breaking change, add app profile
// 10931: app profile struct add 'version' field
// 10946: add capabilities
// 10977: change groups_count and groups to avoid overflow write
// 11071: Fix the issue of failing to set a custom SELinux type.
const val MINIMAL_SUPPORTED_KERNEL = 11071
const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.5"
// 11640: Support query working mode, LKM or GKI
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
// 12040: Support disable sucompat mode
const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
const val KERNEL_SU_DOMAIN = "u:r:su:s0"
const val MINIMAL_SUPPORTED_KPM = 12800
const val MINIMAL_SUPPORTED_DYNAMIC_SIGN = 13215
const val ROOT_UID = 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 {
System.loadLibrary("zakosign")
System.loadLibrary("zako")
}
// become root manager, return true if success.
external fun becomeManager(pkg: String?): Boolean
val version: Int
external get
// get the uid list of allowed su processes.
val allowList: IntArray
external get
val isSafeMode: Boolean
external get
val isLkmMode: Boolean
external get
external fun uidShouldUmount(uid: Int): Boolean
/**
* Get the profile of the given package.
* @param key usually the package name
* @return return null if failed.
*/
external fun getAppProfile(key: String?, uid: Int): Profile
external fun setAppProfile(profile: Profile?): Boolean
/**
* `su` compat mode can be disabled temporarily.
* 0: disabled
* 1: enabled
* negative : error
*/
external fun isSuEnabled(): Boolean
external fun setSuEnabled(enabled: Boolean): Boolean
external fun isKPMEnabled(): Boolean
external fun getHookType(): String
/**
* Get SUSFS feature status from kernel
* @return SusfsFeatureStatus object containing all feature states, or null if failed
*/
external fun getSusfsFeatureStatus(): SusfsFeatureStatus?
/**
* Set dynamic signature configuration
* @param size APK signature size
* @param hash APK signature hash (64 character hex string)
* @return true if successful, false otherwise
*/
external fun setDynamicSign(size: Int, hash: String): Boolean
/**
* Get current dynamic signature configuration
* @return DynamicSignConfig object containing current configuration, or null if not set
*/
external fun getDynamicSign(): DynamicSignConfig?
/**
* Clear dynamic signature configuration
* @return true if successful, false otherwise
*/
external fun clearDynamicSign(): Boolean
/**
* Get active managers list when dynamic sign 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 NOBODY_UID = 9999
fun setDefaultUmountModules(umountModules: Boolean): Boolean {
Profile(
NON_ROOT_DEFAULT_PROFILE_KEY,
NOBODY_UID,
false,
umountModules = umountModules
).let {
return setAppProfile(it)
}
}
fun isDefaultUmountModules(): Boolean {
getAppProfile(NON_ROOT_DEFAULT_PROFILE_KEY, NOBODY_UID).let {
return it.umountModules
}
}
fun requireNewKernel(): Boolean {
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 DynamicSignConfig(
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
@Parcelize
@Keep
data class Profile(
// and there is a default profile for root and non-root
val name: String,
// current uid for the package, this is convivent for kernel to check
// if the package name doesn't match uid, then it should be invalidated.
val currentUid: Int = 0,
// if this is true, kernel will grant root permission to this package
val allowSu: Boolean = false,
// these are used for root profile
val rootUseDefault: Boolean = true,
val rootTemplate: String? = null,
val uid: Int = ROOT_UID,
val gid: Int = ROOT_GID,
val groups: List<Int> = mutableListOf(),
val capabilities: List<Int> = mutableListOf(),
val context: String = KERNEL_SU_DOMAIN,
val namespace: Int = Namespace.INHERITED.ordinal,
val nonRootUseDefault: Boolean = true,
val umountModules: Boolean = true,
var rules: String = "", // this field is save in ksud!!
) : Parcelable {
enum class Namespace {
INHERITED,
GLOBAL,
INDIVIDUAL,
}
constructor() : this("")
}
}

View File

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

View File

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

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

@@ -0,0 +1,269 @@
package com.sukisu.ultra.ui
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import io.sukisu.ultra.UltraToolInstall
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.theme.*
import zako.zako.zako.zakoui.activity.util.*
import zako.zako.zako.zakoui.activity.component.BottomBar
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.webui.initPlatform
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
private lateinit var superUserViewModel: SuperUserViewModel
private lateinit var homeViewModel: HomeViewModel
internal val settingsStateFlow = MutableStateFlow(SettingsState())
data class SettingsState(
val isHideOtherInfo: Boolean = false,
val showKpmInfo: Boolean = false
)
private lateinit var themeChangeObserver: ThemeChangeContentObserver
// 添加标记避免重复初始化
private var isInitialized = false
override fun attachBaseContext(newBase: Context) {
val context = LocaleUtils.applyLocale(newBase)
super.attachBaseContext(context)
}
override fun onCreate(savedInstanceState: Bundle?) {
try {
// 确保应用正确的语言设置
LocaleUtils.applyLanguageSetting(this)
// 应用自定义 DPI
DisplayUtils.applyCustomDpi(this)
// Enable edge to edge
enableEdgeToEdge()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
}
super.onCreate(savedInstanceState)
// 使用标记控制初始化流程
if (!isInitialized) {
initializeViewModels()
initializeData()
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()
}
}
lifecycleScope.launch {
try {
homeViewModel.initializeData()
} catch (e: Exception) {
e.printStackTrace()
}
}
// 数据刷新协程
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
// 初始化主题相关设置
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
val isManager = AppData.isManager(ksuApp.packageName)
if (isManager) {
install()
UltraToolInstall.tryToInstall()
}
}
override fun onResume() {
try {
super.onResume()
LocaleUtils.applyLanguageSetting(this)
ThemeUtils.onActivityResume()
// 仅在需要时刷新数据
if (isInitialized) {
refreshData()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun refreshData() {
lifecycleScope.launch {
try {
superUserViewModel.fetchAppList()
homeViewModel.initializeData()
DataRefreshUtils.refreshData(lifecycleScope)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun onPause() {
try {
super.onPause()
ThemeUtils.onActivityPause(this)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onDestroy() {
try {
ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
super.onDestroy()
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
try {
super.onConfigurationChanged(newConfig)
LocaleUtils.applyLanguageSetting(this)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -1,4 +1,4 @@
package shirkneko.zako.sukisu.ui.component
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
@@ -31,8 +31,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import shirkneko.zako.sukisu.BuildConfig
import shirkneko.zako.sukisu.R
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.R
@Preview
@Composable
@@ -98,8 +98,11 @@ private fun AboutCardContent() {
val annotatedString = AnnotatedString.Companion.fromHtml(
htmlString = stringResource(
id = R.string.about_source_code,
"<b><a href=\"https://github.com/ShirkNeko/KernelSU\">GitHub</a></b>",
"<b><a href=\"https://t.me/SukiKSU\">Telegram</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>怡子曰曰</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(
style = SpanStyle(

View File

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

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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,111 @@
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role
import com.dergoogler.mmrl.ui.component.LabelItem
import com.dergoogler.mmrl.ui.component.text.TextRow
import com.sukisu.ultra.ui.theme.CardConfig
@Composable
fun SwitchItem(
icon: ImageVector? = null,
title: String,
summary: String? = null,
checked: Boolean,
enabled: Boolean = true,
beta: Boolean = false,
onCheckedChange: (Boolean) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
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(
headlineContent = {
Text(title)
},
leadingContent = {
RadioButton(selected = selected, onClick = onClick)
}
)
}

View File

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

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

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

@@ -1,4 +1,4 @@
package shirkneko.zako.sukisu.ui.component.profile
package com.sukisu.ultra.ui.component.profile
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.OutlinedTextField
@@ -11,9 +11,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import shirkneko.zako.sukisu.Natives
import shirkneko.zako.sukisu.R
import shirkneko.zako.sukisu.ui.component.SwitchItem
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.SwitchItem
@Composable
fun AppProfileConfig(
@@ -31,7 +31,6 @@ fun AppProfileConfig(
onValueChange = { onProfileChange(profile.copy(name = it)) }
)
}
SwitchItem(
title = stringResource(R.string.profile_umount_modules),
summary = stringResource(R.string.profile_umount_modules_summary),

View File

@@ -1,4 +1,4 @@
package shirkneko.zako.sukisu.ui.component.profile
package com.sukisu.ultra.ui.component.profile
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -42,12 +43,14 @@ import com.maxkeppeler.sheets.input.models.ValidationResult
import com.maxkeppeler.sheets.list.ListDialog
import com.maxkeppeler.sheets.list.models.ListOption
import com.maxkeppeler.sheets.list.models.ListSelection
import shirkneko.zako.sukisu.Natives
import shirkneko.zako.sukisu.R
import shirkneko.zako.sukisu.profile.Capabilities
import shirkneko.zako.sukisu.profile.Groups
import shirkneko.zako.sukisu.ui.component.rememberCustomDialog
import shirkneko.zako.sukisu.ui.util.isSepolicyValid
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.profile.Capabilities
import com.sukisu.ultra.profile.Groups
import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.util.isSepolicyValid
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -206,6 +209,12 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
}
val selection = HashSet(selected)
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
ListDialog(
state = rememberUseCaseState(visible = true, onFinishedRequest = {
closeSelection(selection)
@@ -229,6 +238,7 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
}
)
}
}
OutlinedCard(
modifier = Modifier
@@ -278,6 +288,12 @@ fun CapsPanel(
}
val selection = HashSet(selected)
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
ListDialog(
state = rememberUseCaseState(visible = true, onFinishedRequest = {
closeSelection(selection)
@@ -300,6 +316,7 @@ fun CapsPanel(
}
)
}
}
OutlinedCard(
modifier = Modifier
@@ -425,8 +442,15 @@ private fun SELinuxPanel(
)
)
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
InputDialog(
state = rememberUseCaseState(visible = true,
state = rememberUseCaseState(
visible = true,
onFinishedRequest = {
onSELinuxChange(domain, rules)
},
@@ -444,6 +468,7 @@ private fun SELinuxPanel(
)
)
}
}
ListItem(headlineContent = {
OutlinedTextField(

View File

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

View File

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

View File

@@ -1,8 +1,9 @@
package shirkneko.zako.sukisu.ui.screen
package com.sukisu.ultra.ui.screen
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Archive
import androidx.compose.material.icons.outlined.*
import androidx.compose.ui.graphics.vector.ImageVector
import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination
@@ -11,7 +12,7 @@ import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDe
import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination
import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination
import com.ramcosta.composedestinations.spec.DirectionDestinationSpec
import shirkneko.zako.sukisu.R
import com.sukisu.ultra.R
enum class BottomBarDestination(
val direction: DirectionDestinationSpec,
@@ -21,8 +22,8 @@ enum class BottomBarDestination(
val rootRequired: Boolean,
) {
Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false),
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true),
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true),
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true),
Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Archive, Icons.Outlined.Archive, true),
SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.AdminPanelSettings, Icons.Outlined.AdminPanelSettings, true),
Module(ModuleScreenDestination, R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension, true),
Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false),
}

View File

@@ -1,15 +1,22 @@
package shirkneko.zako.sukisu.ui.screen
package com.sukisu.ultra.ui.screen
import android.os.Environment
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.only
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -30,17 +37,16 @@ import androidx.compose.ui.input.key.key
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import shirkneko.zako.sukisu.R
import shirkneko.zako.sukisu.ui.component.KeyEventBlocker
import shirkneko.zako.sukisu.ui.util.LocalSnackbarHost
import shirkneko.zako.sukisu.ui.util.runModuleAction
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.KeyEventBlocker
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.runModuleAction
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
@@ -55,7 +61,11 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var actionResult: Boolean
var isActionRunning by rememberSaveable { mutableStateOf(true) }
BackHandler(enabled = isActionRunning) {
// Disable back button if action is running
}
LaunchedEffect(Unit) {
if (text.isNotEmpty()) {
@@ -76,20 +86,17 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
onStderr = {
logContent.append(it).append("\n")
}
).let {
actionResult = it
)
}
}
if (actionResult) navigator.popBackStack()
isActionRunning = false
}
Scaffold(
topBar = {
TopBar(
onBack = dropUnlessResumed {
navigator.popBackStack()
},
isActionRunning = isActionRunning,
onSave = {
if (!isActionRunning) {
scope.launch {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
val date = format.format(Date())
@@ -101,8 +108,21 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
snackBarHost.showSnackbar("Log saved to ${file.absolutePath}")
}
}
}
)
},
floatingActionButton = {
if (!isActionRunning) {
ExtendedFloatingActionButton(
text = { Text(text = stringResource(R.string.close)) },
icon = { Icon(Icons.Filled.Close, contentDescription = null) },
onClick = {
navigator.popBackStack()
}
)
}
},
contentWindowInsets = WindowInsets.safeDrawing,
snackbarHost = { SnackbarHost(snackBarHost) }
) { innerPadding ->
KeyEventBlocker {
@@ -130,16 +150,14 @@ fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) {
private fun TopBar(isActionRunning: Boolean, onSave: () -> Unit = {}) {
TopAppBar(
title = { Text(stringResource(R.string.action)) },
navigationIcon = {
IconButton(
onClick = onBack
) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }
},
actions = {
IconButton(onClick = onSave) {
IconButton(
onClick = onSave,
enabled = !isActionRunning
) {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = stringResource(id = R.string.save_log),

View File

@@ -0,0 +1,701 @@
package com.sukisu.ultra.ui.screen
import android.net.Uri
import android.os.Environment
import android.os.Parcelable
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import com.sukisu.ultra.ui.component.KeyEventBlocker
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.viewmodel.ModuleViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
enum class FlashingStatus {
FLASHING,
SUCCESS,
FAILED
}
private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING)
// 添加模块安装状态跟踪
data class ModuleInstallStatus(
val totalModules: Int = 0,
val currentModule: Int = 0,
val currentModuleName: String = "",
val failedModules: MutableList<String> = mutableListOf(),
val verifiedModules: MutableList<String> = mutableListOf() // 添加已验证模块列表
)
private var moduleInstallStatus = mutableStateOf(ModuleInstallStatus())
// 存储模块URI和验证状态的映射
private var moduleVerificationMap = mutableMapOf<Uri, Boolean>()
fun setFlashingStatus(status: FlashingStatus) {
currentFlashingStatus.value = status
}
fun updateModuleInstallStatus(
totalModules: Int? = null,
currentModule: Int? = null,
currentModuleName: String? = null,
failedModule: String? = null,
verifiedModule: String? = null
) {
val current = moduleInstallStatus.value
moduleInstallStatus.value = current.copy(
totalModules = totalModules ?: current.totalModules,
currentModule = currentModule ?: current.currentModule,
currentModuleName = currentModuleName ?: current.currentModuleName
)
if (failedModule != null) {
val updatedFailedModules = current.failedModules.toMutableList()
updatedFailedModules.add(failedModule)
moduleInstallStatus.value = moduleInstallStatus.value.copy(
failedModules = updatedFailedModules
)
}
if (verifiedModule != null) {
val updatedVerifiedModules = current.verifiedModules.toMutableList()
updatedVerifiedModules.add(verifiedModule)
moduleInstallStatus.value = moduleInstallStatus.value.copy(
verifiedModules = updatedVerifiedModules
)
}
}
fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) {
moduleVerificationMap[uri] = isVerified
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Destination<RootGraph>
fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) {
val context = LocalContext.current
var text by rememberSaveable { mutableStateOf("") }
var tempText: String
val logContent = rememberSaveable { StringBuilder() }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
// 添加状态跟踪是否已经完成刷写
var hasFlashCompleted by rememberSaveable { mutableStateOf(false) }
var hasExecuted by rememberSaveable { mutableStateOf(false) }
// 更新模块状态管理
var hasUpdateExecuted by rememberSaveable { mutableStateOf(false) }
var hasUpdateCompleted by rememberSaveable { mutableStateOf(false) }
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
val scrollState = rememberScrollState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val viewModel: ModuleViewModel = viewModel()
val errorCodeString = stringResource(R.string.error_code)
val checkLogString = stringResource(R.string.check_log)
val logSavedString = stringResource(R.string.log_saved)
val installingModuleString = stringResource(R.string.installing_module)
// 当前模块安装状态
val currentStatus = moduleInstallStatus.value
// 重置状态
LaunchedEffect(flashIt) {
when (flashIt) {
is FlashIt.FlashModules -> {
if (flashIt.currentIndex == 0) {
moduleInstallStatus.value = ModuleInstallStatus(
totalModules = flashIt.uris.size,
currentModule = 1
)
hasFlashCompleted = false
hasExecuted = false
moduleVerificationMap.clear()
}
}
is FlashIt.FlashModuleUpdate -> {
hasUpdateCompleted = false
hasUpdateExecuted = false
}
else -> {
hasFlashCompleted = false
hasExecuted = false
}
}
}
// 处理更新模块安装
LaunchedEffect(flashIt) {
if (flashIt !is FlashIt.FlashModuleUpdate) return@LaunchedEffect
if (hasUpdateExecuted || hasUpdateCompleted || text.isNotEmpty()) {
return@LaunchedEffect
}
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) {
setFlashingStatus(FlashingStatus.FLASHING)
if (flashIt is FlashIt.FlashModules) {
try {
val currentUri = flashIt.uris[flashIt.currentIndex]
val moduleName = getModuleNameFromUri(context, currentUri)
updateModuleInstallStatus(
currentModuleName = moduleName
)
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, moduleName)
logContent.append(text).append("\n")
} catch (_: Exception) {
text = installingModuleString.format(flashIt.currentIndex + 1, flashIt.uris.size, "Module")
logContent.append(text).append("\n")
}
}
flashIt(flashIt, onFinish = { showReboot, code ->
if (code != 0) {
text += "$errorCodeString $code.\n$checkLogString\n"
setFlashingStatus(FlashingStatus.FAILED)
if (flashIt is FlashIt.FlashModules) {
updateModuleInstallStatus(
failedModule = moduleInstallStatus.value.currentModuleName
)
}
} else {
setFlashingStatus(FlashingStatus.SUCCESS)
// 处理模块安装成功后的验证标志
when (flashIt) {
is FlashIt.FlashModule -> {
val isVerified = moduleVerificationMap[flashIt.uri] ?: false
ModuleOperationUtils.handleModuleInstallSuccess(context, flashIt.uri, isVerified)
if (isVerified) {
updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName)
}
}
is FlashIt.FlashModules -> {
val currentUri = flashIt.uris[flashIt.currentIndex]
val isVerified = moduleVerificationMap[currentUri] ?: false
ModuleOperationUtils.handleModuleInstallSuccess(context, currentUri, isVerified)
if (isVerified) {
updateModuleInstallStatus(verifiedModule = moduleInstallStatus.value.currentModuleName)
}
}
else -> {}
}
viewModel.markNeedRefresh()
}
if (showReboot) {
text += "\n\n\n"
showFloatAction = true
}
hasFlashCompleted = true
if (flashIt is FlashIt.FlashModules && flashIt.currentIndex < flashIt.uris.size - 1) {
val nextFlashIt = flashIt.copy(
currentIndex = flashIt.currentIndex + 1
)
scope.launch {
kotlinx.coroutines.delay(500)
navigator.navigate(FlashScreenDestination(nextFlashIt))
}
}
}, onStdout = {
tempText = "$it\n"
if (tempText.startsWith("")) { // clear command
text = tempText.substring(6)
} else {
text += tempText
}
logContent.append(it).append("\n")
}, onStderr = {
logContent.append(it).append("\n")
})
}
}
val onBack: () -> Unit = {
val canGoBack = when (flashIt) {
is FlashIt.FlashModuleUpdate -> currentFlashingStatus.value != FlashingStatus.FLASHING
else -> currentFlashingStatus.value != FlashingStatus.FLASHING
}
if (canGoBack) {
if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.navigate(ModuleScreenDestination)
} else {
viewModel.markNeedRefresh()
viewModel.fetchModuleList()
navigator.popBackStack()
}
}
}
BackHandler(enabled = true) {
onBack()
}
Scaffold(
topBar = {
TopBar(
currentFlashingStatus.value,
currentStatus,
onBack = onBack,
onSave = {
scope.launch {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
val date = format.format(Date())
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"KernelSU_install_log_${date}.log"
)
file.writeText(logContent.toString())
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
}
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
if (showFloatAction) {
ExtendedFloatingActionButton(
onClick = {
scope.launch {
withContext(Dispatchers.IO) {
reboot()
}
}
},
icon = {
Icon(
Icons.Filled.Refresh,
contentDescription = stringResource(id = R.string.reboot)
)
},
text = {
Text(text = stringResource(id = R.string.reboot))
},
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
expanded = true
)
}
},
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
containerColor = MaterialTheme.colorScheme.background
) { innerPadding ->
KeyEventBlocker {
it.key == Key.VolumeDown || it.key == Key.VolumeUp
}
Column(
modifier = Modifier
.fillMaxSize(1f)
.padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
if (flashIt is FlashIt.FlashModules) {
ModuleInstallProgressBar(
currentIndex = flashIt.currentIndex + 1,
totalCount = flashIt.uris.size,
currentModuleName = currentStatus.currentModuleName,
status = currentFlashingStatus.value,
failedModules = currentStatus.failedModules
)
Spacer(modifier = Modifier.height(8.dp))
}
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(scrollState)
) {
LaunchedEffect(text) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Text(
modifier = Modifier.padding(16.dp),
text = text,
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
// 显示模块安装进度条和状态
@Composable
fun ModuleInstallProgressBar(
currentIndex: Int,
totalCount: Int,
currentModuleName: String,
status: FlashingStatus,
failedModules: List<String>
) {
val progressColor = when(status) {
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
}
val progress = animateFloatAsState(
targetValue = currentIndex.toFloat() / totalCount.toFloat(),
label = "InstallProgress"
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// 模块名称和进度
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = currentModuleName.ifEmpty { stringResource(R.string.module) },
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "$currentIndex/$totalCount",
style = MaterialTheme.typography.titleMedium
)
}
Spacer(modifier = Modifier.height(8.dp))
// 进度条
LinearProgressIndicator(
progress = { progress.value },
modifier = Modifier
.fillMaxWidth()
.height(8.dp),
color = progressColor,
trackColor = MaterialTheme.colorScheme.surfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
// 失败模块列表
AnimatedVisibility(
visible = failedModules.isNotEmpty(),
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = stringResource(R.string.module_failed_count, failedModules.size),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
Spacer(modifier = Modifier.height(4.dp))
// 失败模块列表
Column(
modifier = Modifier
.fillMaxWidth()
.background(
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
shape = MaterialTheme.shapes.small
)
.padding(8.dp)
) {
failedModules.forEach { moduleName ->
Text(
text = "$moduleName",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
status: FlashingStatus,
moduleStatus: ModuleInstallStatus = ModuleInstallStatus(),
onBack: () -> Unit,
onSave: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = CardConfig.cardAlpha
val statusColor = when(status) {
FlashingStatus.FLASHING -> MaterialTheme.colorScheme.primary
FlashingStatus.SUCCESS -> MaterialTheme.colorScheme.tertiary
FlashingStatus.FAILED -> MaterialTheme.colorScheme.error
}
TopAppBar(
title = {
Column {
Text(
text = stringResource(
when (status) {
FlashingStatus.FLASHING -> R.string.flashing
FlashingStatus.SUCCESS -> R.string.flash_success
FlashingStatus.FAILED -> R.string.flash_failed
}
),
style = MaterialTheme.typography.titleLarge,
color = statusColor
)
if (moduleStatus.failedModules.isNotEmpty()) {
Text(
text = stringResource(R.string.module_failed_count, moduleStatus.failedModules.size),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error
)
}
}
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = stringResource(id = R.string.save_log),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String {
return withContext(Dispatchers.IO) {
try {
if (uri == Uri.EMPTY) {
return@withContext context.getString(R.string.unknown_module)
}
if (!ModuleUtils.isUriAccessible(context, uri)) {
return@withContext context.getString(R.string.unknown_module)
}
ModuleUtils.extractModuleName(context, uri)
} catch (_: Exception) {
context.getString(R.string.unknown_module)
}
}
}
@Parcelize
sealed class FlashIt : Parcelable {
data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt()
data class FlashModule(val uri: Uri) : FlashIt()
data class FlashModules(val uris: List<Uri>, val currentIndex: Int = 0) : FlashIt()
data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新
data object FlashRestore : 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(
flashIt: FlashIt,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
) {
when (flashIt) {
is FlashIt.FlashBoot -> installBoot(
flashIt.boot,
flashIt.lkm,
flashIt.ota,
onFinish,
onStdout,
onStderr
)
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
is FlashIt.FlashModules -> {
if (flashIt.uris.isEmpty() || flashIt.currentIndex >= flashIt.uris.size) {
onFinish(false, 0)
return
}
val currentUri = flashIt.uris[flashIt.currentIndex]
onStdout("\n")
flashModule(currentUri, onFinish, onStdout, onStderr)
}
is FlashIt.FlashModuleUpdate -> {
onFinish(false, 0)
}
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)
}
}
@Preview
@Composable
fun FlashScreenPreview() {
FlashScreen(EmptyDestinationsNavigator, FlashIt.FlashUninstall)
}

View File

@@ -0,0 +1,912 @@
package com.sukisu.ultra.ui.screen
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.PowerManager
import android.system.Os
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.TaskAlt
import androidx.compose.material.icons.outlined.Warning
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.pm.PackageInfoCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination
import com.ramcosta.composedestinations.generated.destinations.SuSFSConfigScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.KernelVersion
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.rememberConfirmDialog
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.getCardColors
import com.sukisu.ultra.ui.theme.getCardElevation
import com.sukisu.ultra.ui.util.checkNewVersion
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.random.Random
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Destination<RootGraph>(start = true)
@Composable
fun HomeScreen(navigator: DestinationsNavigator) {
val context = LocalContext.current
val viewModel = viewModel<HomeViewModel>()
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(key1 = navigator) {
coroutineScope.launch {
viewModel.refreshAllData(context)
}
}
LaunchedEffect(Unit) {
viewModel.loadUserSettings(context)
viewModel.initializeData()
viewModel.checkForUpdates(context)
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val scrollState = rememberScrollState()
Scaffold(
topBar = {
TopBar(
scrollBehavior = scrollBehavior,
navigator = navigator
)
},
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
)
) { innerPadding ->
val pullRefreshState = rememberPullRefreshState(
refreshing = false,
onRefresh = {
coroutineScope.launch {
viewModel.refreshAllData(context)
}
}
)
Box(
modifier = Modifier
.padding(innerPadding)
.fillMaxSize()
.pullRefresh(pullRefreshState)
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState)
.padding(top = 12.dp, start = 16.dp, end = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
StatusCard(
systemStatus = viewModel.systemStatus,
onClickInstall = {
navigator.navigate(InstallScreenDestination)
}
)
if (viewModel.systemStatus.requireNewKernel) {
WarningCard(
stringResource(id = R.string.require_kernel_version).format(
Natives.getSimpleVersionFull(),
Natives.MINIMAL_SUPPORTED_KERNEL_FULL
)
)
}
if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) {
WarningCard(
stringResource(id = R.string.grant_root_failed)
)
}
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true)
if (checkUpdate) {
UpdateCard()
}
InfoCard(
systemInfo = viewModel.systemInfo,
isSimpleMode = viewModel.isSimpleMode,
isHideSusfsStatus = viewModel.isHideSusfsStatus,
isHideZygiskImplement = viewModel.isHideZygiskImplement,
showKpmInfo = viewModel.showKpmInfo,
lkmMode = viewModel.systemStatus.lkmMode,
)
if (!viewModel.isSimpleMode) {
if (!viewModel.isHideLinkCard) {
ContributionCard()
DonateCard()
LearnMoreCard()
}
}
Spacer(Modifier.height(16.dp))
}
}
}
}
@Composable
fun UpdateCard() {
val context = LocalContext.current
val latestVersionInfo = LatestVersionInfo()
val newVersion by produceState(initialValue = latestVersionInfo) {
value = withContext(Dispatchers.IO) {
checkNewVersion()
}
}
val currentVersionCode = getManagerVersion(context).second
val newVersionCode = newVersion.versionCode
val newVersionUrl = newVersion.downloadUrl
val changelog = newVersion.changelog
val uriHandler = LocalUriHandler.current
val title = stringResource(id = R.string.module_changelog)
val updateText = stringResource(id = R.string.module_update)
AnimatedVisibility(
visible = newVersionCode > currentVersionCode,
enter = fadeIn() + expandVertically(
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
),
exit = shrinkVertically() + fadeOut()
) {
val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) })
WarningCard(
message = stringResource(id = R.string.new_version_available).format(newVersionCode),
color = MaterialTheme.colorScheme.outlineVariant,
onClick = {
if (changelog.isEmpty()) {
uriHandler.openUri(newVersionUrl)
} else {
updateDialog.showConfirm(
title = title,
content = changelog,
markdown = true,
confirm = updateText
)
}
}
)
}
}
@Composable
fun RebootDropdownItem(@StringRes id: Int, reason: String = "") {
DropdownMenuItem(
text = { Text(stringResource(id)) },
onClick = { reboot(reason) })
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null,
navigator: DestinationsNavigator
) {
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
TopAppBar(
title = {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
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) }
KsuIsValid {
IconButton(onClick = {
showDropdown = true
}) {
Icon(
imageVector = Icons.Filled.PowerSettingsNew,
contentDescription = stringResource(id = R.string.reboot)
)
DropdownMenu(expanded = showDropdown, onDismissRequest = {
showDropdown = false
}) {
RebootDropdownItem(id = R.string.reboot)
val pm =
LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) {
RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace")
}
RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery")
RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader")
RebootDropdownItem(id = R.string.reboot_download, reason = "download")
RebootDropdownItem(id = R.string.reboot_edl, reason = "edl")
}
}
}
},
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}
@Composable
private fun StatusCard(
systemStatus: HomeViewModel.SystemStatus,
onClickInstall: () -> Unit = {}
) {
ElevatedCard(
colors = getCardColors(
if (systemStatus.ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer
else MaterialTheme.colorScheme.errorContainer
),
elevation = getCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
if (systemStatus.isRootAvailable || systemStatus.kernelVersion.isGKI()) {
onClickInstall()
}
}
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
when {
systemStatus.ksuVersion != null -> {
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 == null && systemStatus.kernelVersion.isGKI1() -> "GKI 1.0"
systemStatus.lkmMode == false || systemStatus.kernelVersion.isGKI() -> "GKI 2.0"
else -> "N-GKI"
}
Icon(
Icons.Outlined.TaskAlt,
contentDescription = stringResource(R.string.home_working),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.size(28.dp)
.padding(
horizontal = 4.dp
),
)
Column(Modifier.padding(start = 20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = workingModeText,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.width(8.dp))
// 工作模式标签
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
) {
Text(
text = workingModeSurfaceText,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
color = MaterialTheme.colorScheme.onPrimary
)
}
Spacer(Modifier.width(6.dp))
// 架构标签
if (Os.uname().machine != "aarch64") {
Surface(
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
) {
Text(
text = Os.uname().machine,
style = MaterialTheme.typography.labelMedium,
modifier = Modifier.padding(
horizontal = 6.dp,
vertical = 2.dp
),
color = MaterialTheme.colorScheme.onPrimary
)
}
}
}
val isHideVersion = LocalContext.current.getSharedPreferences(
"settings",
Context.MODE_PRIVATE
)
.getBoolean("is_hide_version", false)
if (!isHideVersion) {
Spacer(Modifier.height(4.dp))
systemStatus.ksuFullVersion?.let {
Text(
text = stringResource(R.string.home_working_version, it),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.secondary,
)
}
}
}
}
systemStatus.kernelVersion.isGKI() -> {
Icon(
Icons.Outlined.Warning,
contentDescription = stringResource(R.string.home_not_installed),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier
.size(28.dp)
.padding(
horizontal = 4.dp
),
)
Column(Modifier.padding(start = 20.dp)) {
Text(
text = stringResource(R.string.home_not_installed),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_install),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
else -> {
Icon(
Icons.Outlined.Block,
contentDescription = stringResource(R.string.home_unsupported),
tint = MaterialTheme.colorScheme.error,
modifier = Modifier
.size(28.dp)
.padding(
horizontal = 4.dp
),
)
Column(Modifier.padding(start = 20.dp)) {
Text(
text = stringResource(R.string.home_unsupported),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.error
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_unsupported_reason),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer
)
}
}
}
}
}
}
@Composable
fun WarningCard(
message: String,
color: Color = MaterialTheme.colorScheme.error,
onClick: (() -> Unit)? = null
) {
ElevatedCard(
colors = getCardColors(color),
elevation = getCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = message,
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
@Composable
fun ContributionCard() {
val uriHandler = LocalUriHandler.current
val links = listOf("https://github.com/ShirkNeko", "https://github.com/udochina")
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = getCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
val randomIndex = Random.nextInt(links.size)
uriHandler.openUri(links[randomIndex])
}
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(R.string.home_ContributionCard_kernelsu),
style = MaterialTheme.typography.titleSmall,
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_ContributionCard_kernelsu),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
@Composable
fun LearnMoreCard() {
val uriHandler = LocalUriHandler.current
val url = stringResource(R.string.home_learn_kernelsu_url)
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri(url)
}
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(R.string.home_learn_kernelsu),
style = MaterialTheme.typography.titleSmall,
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_click_to_learn_kernelsu),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
@Composable
fun DonateCard() {
val uriHandler = LocalUriHandler.current
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = getCardElevation(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri("https://patreon.com/weishu")
}
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = stringResource(R.string.home_support_title),
style = MaterialTheme.typography.titleSmall,
)
Spacer(Modifier.height(4.dp))
Text(
text = stringResource(R.string.home_support_content),
style = MaterialTheme.typography.bodyMedium,
)
}
}
}
}
@Composable
private fun InfoCard(
systemInfo: HomeViewModel.SystemInfo,
isSimpleMode: Boolean,
isHideSusfsStatus: Boolean,
isHideZygiskImplement: Boolean,
showKpmInfo: Boolean,
lkmMode: Boolean?
) {
ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainer),
elevation = getCardElevation(),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp),
) {
@Composable
fun InfoCardItem(
label: String,
content: String,
icon: ImageVector? = null,
) {
Row(
verticalAlignment = Alignment.Top,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
) {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = label,
modifier = Modifier
.size(28.dp)
.padding(vertical = 4.dp),
)
}
Spacer(modifier = Modifier.width(16.dp))
Column(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
)
Text(
text = content,
style = MaterialTheme.typography.bodyMedium,
softWrap = true
)
}
}
}
InfoCardItem(
stringResource(R.string.home_kernel),
systemInfo.kernelRelease,
icon = Icons.Default.Memory,
)
if (!isSimpleMode) {
InfoCardItem(
stringResource(R.string.home_android_version),
systemInfo.androidVersion,
icon = Icons.Default.Android,
)
}
InfoCardItem(
stringResource(R.string.home_device_model),
systemInfo.deviceModel,
icon = Icons.Default.PhoneAndroid,
)
InfoCardItem(
stringResource(R.string.home_manager_version),
"${systemInfo.managerVersion.first} (${systemInfo.managerVersion.second.toInt()})",
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_signature)})"
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(
stringResource(R.string.home_selinux_status),
systemInfo.seLinuxStatus,
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) {
// 根据showKpmInfo决定是否显示KPM信息
if (lkmMode != true && !showKpmInfo) {
val displayVersion =
if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) {
val statusText = if (Natives.isKPMEnabled()) {
stringResource(R.string.kernel_patched)
} else {
stringResource(R.string.kernel_not_enabled)
}
"${stringResource(R.string.not_supported)} ($statusText)"
} else {
"${stringResource(R.string.supported)} (${systemInfo.kpmVersion})"
}
InfoCardItem(
stringResource(R.string.home_kpm_version),
displayVersion,
icon = Icons.Default.Archive
)
}
}
if (!isSimpleMode && !isHideSusfsStatus &&
systemInfo.suSFSStatus == "Supported" &&
systemInfo.suSFSVersion.isNotEmpty()
) {
val infoText = SuSFSInfoText(systemInfo)
InfoCardItem(
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> {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)
return Pair(packageInfo.versionName!!, versionCode)
}
@Preview
@Composable
private fun StatusCardPreview() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
StatusCard(
HomeViewModel.SystemStatus(
isManager = true,
ksuVersion = 1,
lkmMode = null,
kernelVersion = KernelVersion(5, 10, 101),
isRootAvailable = true
)
)
StatusCard(
HomeViewModel.SystemStatus(
isManager = true,
ksuVersion = 20000,
lkmMode = true,
kernelVersion = KernelVersion(5, 10, 101),
isRootAvailable = true
)
)
StatusCard(
HomeViewModel.SystemStatus(
isManager = false,
ksuVersion = null,
lkmMode = true,
kernelVersion = KernelVersion(5, 10, 101),
isRootAvailable = false
)
)
StatusCard(
HomeViewModel.SystemStatus(
isManager = false,
ksuVersion = null,
lkmMode = false,
kernelVersion = KernelVersion(4, 10, 101),
isRootAvailable = false
)
)
}
}
@Preview
@Composable
private fun WarningCardPreview() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
WarningCard(message = "Warning message")
WarningCard(
message = "Warning message ",
MaterialTheme.colorScheme.outlineVariant,
onClick = {})
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,818 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Undo
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import androidx.core.content.edit
import com.maxkeppeker.sheets.core.models.base.IconSource
import com.maxkeppeler.sheets.list.models.ListOption
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.getBugreportFile
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import com.sukisu.ultra.ui.component.KsuIsValid
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
private val SPACING_SMALL = 3.dp
private val SPACING_MEDIUM = 8.dp
private val SPACING_LARGE = 16.dp
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun SettingScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
var selectedEngine by rememberSaveable {
mutableStateOf(
prefs.getString("webui_engine", "default") ?: "default"
)
}
Scaffold(
// containerColor = MaterialTheme.colorScheme.surfaceBright,
topBar = {
TopBar(scrollBehavior = scrollBehavior)
},
snackbarHost = { SnackbarHost(snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
) { paddingValues ->
val aboutDialog = rememberCustomDialog {
AboutDialog(it)
}
val loadingDialog = rememberLoadingDialog()
Column(
modifier = Modifier
.padding(paddingValues)
.nestedScroll(scrollBehavior.nestedScrollConnection)
.verticalScroll(rememberScrollState())
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val exportBugreportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/gzip")
) { uri: Uri? ->
if (uri == null) return@rememberLauncherForActivityResult
scope.launch(Dispatchers.IO) {
loadingDialog.show()
context.contentResolver.openOutputStream(uri)?.use { output ->
getBugreportFile(context).inputStream().use {
it.copyTo(output)
}
}
loadingDialog.hide()
snackBarHost.showSnackbar(context.getString(R.string.log_saved))
}
}
// 配置卡片
KsuIsValid {
SettingsGroupCard(
title = stringResource(R.string.configuration),
content = {
// 配置文件模板入口
SettingItem(
icon = Icons.Filled.Fence,
title = stringResource(R.string.settings_profile_template),
summary = stringResource(R.string.settings_profile_template_summary),
onClick = {
navigator.navigate(AppProfileTemplateScreenDestination)
}
)
// 卸载模块开关
var umountChecked by rememberSaveable {
mutableStateOf(Natives.isDefaultUmountModules())
}
SwitchItem(
icon = Icons.Filled.FolderDelete,
title = stringResource(R.string.settings_umount_modules_default),
summary = stringResource(R.string.settings_umount_modules_default_summary),
checked = umountChecked,
onCheckedChange = { enabled ->
if (Natives.setDefaultUmountModules(enabled)) {
umountChecked = enabled
}
}
)
// SU 禁用开关
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled())
}
SwitchItem(
icon = Icons.Filled.RemoveModerator,
title = stringResource(R.string.settings_disable_su),
summary = stringResource(R.string.settings_disable_su_summary),
checked = isSuDisabled,
onCheckedChange = { enabled ->
val shouldEnable = !enabled
if (Natives.setSuEnabled(shouldEnable)) {
isSuDisabled = enabled
}
}
)
}
// 强制签名验证开关
var forceSignatureVerification by rememberSaveable {
mutableStateOf(prefs.getBoolean("force_signature_verification", false))
}
SwitchItem(
icon = Icons.Filled.Security,
title = stringResource(R.string.module_signature_verification),
summary = stringResource(R.string.module_signature_verification_summary),
checked = forceSignatureVerification,
onCheckedChange = { enabled ->
prefs.edit { putBoolean("force_signature_verification", enabled) }
forceSignatureVerification = enabled
}
)
}
)
}
// 应用设置卡片
SettingsGroupCard(
title = stringResource(R.string.app_settings),
content = {
// 更新检查开关
var checkUpdate by rememberSaveable {
mutableStateOf(prefs.getBoolean("check_update", true))
}
SwitchItem(
icon = Icons.Filled.Update,
title = stringResource(R.string.settings_check_update),
summary = stringResource(R.string.settings_check_update_summary),
checked = checkUpdate,
onCheckedChange = { enabled ->
prefs.edit { putBoolean("check_update", enabled) }
checkUpdate = enabled
}
)
// WebUI引擎选择
KsuIsValid {
WebUIEngineSelector(
selectedEngine = selectedEngine,
onEngineSelected = { engine ->
selectedEngine = engine
prefs.edit { putString("webui_engine", engine) }
}
)
}
// Web调试和Web X Eruda 开关
var enableWebDebugging by rememberSaveable {
mutableStateOf(prefs.getBoolean("enable_web_debugging", false))
}
var useWebUIXEruda by rememberSaveable {
mutableStateOf(prefs.getBoolean("use_webuix_eruda", false))
}
KsuIsValid {
SwitchItem(
icon = Icons.Filled.DeveloperMode,
title = stringResource(R.string.enable_web_debugging),
summary = stringResource(R.string.enable_web_debugging_summary),
checked = enableWebDebugging,
onCheckedChange = { enabled ->
prefs.edit { putBoolean("enable_web_debugging", enabled) }
enableWebDebugging = enabled
}
)
AnimatedVisibility(
visible = enableWebDebugging && selectedEngine == "wx",
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SwitchItem(
icon = Icons.Filled.FormatListNumbered,
title = stringResource(R.string.use_webuix_eruda),
summary = stringResource(R.string.use_webuix_eruda_summary),
checked = useWebUIXEruda,
onCheckedChange = { enabled ->
prefs.edit { putBoolean("use_webuix_eruda", enabled) }
useWebUIXEruda = enabled
}
)
}
}
// 更多设置
SettingItem(
icon = Icons.Filled.Settings,
title = stringResource(R.string.more_settings),
summary = stringResource(R.string.more_settings),
onClick = {
navigator.navigate(MoreSettingsScreenDestination)
}
)
}
)
// 工具卡片
SettingsGroupCard(
title = stringResource(R.string.tools),
content = {
var showBottomsheet by remember { mutableStateOf(false) }
SettingItem(
icon = Icons.Filled.BugReport,
title = stringResource(R.string.send_log),
onClick = {
showBottomsheet = true
}
)
if (showBottomsheet) {
LogBottomSheet(
onDismiss = { showBottomsheet = false },
onSaveLog = {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter)
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
showBottomsheet = false
},
onShareLog = {
scope.launch {
val bugreport = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
getBugreportFile(context)
}
}
val uri = FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileprovider",
bugreport
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
setDataAndType(uri, "application/gzip")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(
shareIntent,
context.getString(R.string.send_log)
)
)
showBottomsheet = false
}
}
)
}
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
if (lkmMode) {
UninstallItem(navigator) {
loadingDialog.withLoading(it)
}
}
}
)
// 关于卡片
SettingsGroupCard(
title = stringResource(R.string.about),
content = {
SettingItem(
icon = Icons.Filled.Info,
title = stringResource(R.string.about),
onClick = {
aboutDialog.show()
}
)
}
)
Spacer(modifier = Modifier.height(SPACING_LARGE))
}
}
}
@Composable
private fun SettingsGroupCard(
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
elevation = getCardElevation()
) {
Column(
modifier = Modifier.padding(vertical = SPACING_MEDIUM)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
color = MaterialTheme.colorScheme.primary
)
content()
}
}
}
@Composable
private fun WebUIEngineSelector(
selectedEngine: String,
onEngineSelected: (String) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
val engineOptions = listOf(
"default" to stringResource(R.string.engine_auto_select),
"wx" to stringResource(R.string.engine_force_webuix),
"ksu" to stringResource(R.string.engine_force_ksu)
)
SettingItem(
icon = Icons.Filled.WebAsset,
title = stringResource(R.string.use_webuix),
summary = engineOptions.find { it.first == selectedEngine }?.second
?: stringResource(R.string.engine_auto_select),
onClick = { showDialog = true }
)
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text(stringResource(R.string.use_webuix)) },
text = {
Column {
engineOptions.forEach { (value, label) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onEngineSelected(value)
showDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedEngine == value,
onClick = null
)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(text = label)
}
}
}
},
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LogBottomSheet(
onDismiss: () -> Unit,
onSaveLog: () -> Unit,
onShareLog: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
horizontalArrangement = Arrangement.SpaceEvenly
) {
LogActionButton(
icon = Icons.Filled.Save,
text = stringResource(R.string.save_log),
onClick = onSaveLog
)
LogActionButton(
icon = Icons.Filled.Share,
text = stringResource(R.string.send_log),
onClick = onShareLog
)
}
Spacer(modifier = Modifier.height(SPACING_LARGE))
}
}
@Composable
fun LogActionButton(
icon: ImageVector,
text: String,
onClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.clickable(onClick = onClick)
.padding(SPACING_MEDIUM)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer)
) {
Icon(
imageVector = icon,
contentDescription = text,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(24.dp)
)
}
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Text(
text = text,
style = MaterialTheme.typography.bodyMedium
)
}
}
@Composable
fun SettingItem(
icon: ImageVector,
title: String,
summary: String? = null,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = SPACING_LARGE)
.size(24.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
if (summary != null) {
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium
)
}
}
Icon(
imageVector = Icons.Filled.ChevronRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
@Composable
fun SwitchItem(
icon: ImageVector,
title: String,
summary: String? = null,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onCheckedChange(!checked) }
.padding(horizontal = SPACING_LARGE, vertical = 12.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(end = SPACING_LARGE)
.size(24.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium
)
if (summary != null) {
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = summary,
style = MaterialTheme.typography.bodyMedium
)
}
}
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
}
@Composable
fun UninstallItem(
navigator: DestinationsNavigator,
withLoading: suspend (suspend () -> Unit) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val uninstallConfirmDialog = rememberConfirmDialog()
val showTodo = {
Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show()
}
val uninstallDialog = rememberUninstallDialog { uninstallType ->
scope.launch {
val result = uninstallConfirmDialog.awaitConfirm(
title = context.getString(uninstallType.title),
content = context.getString(uninstallType.message)
)
if (result == ConfirmResult.Confirmed) {
withLoading {
when (uninstallType) {
UninstallType.TEMPORARY -> showTodo()
UninstallType.PERMANENT -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashUninstall)
)
UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate(
FlashScreenDestination(FlashIt.FlashRestore)
)
UninstallType.NONE -> Unit
}
}
}
}
}
SettingItem(
icon = Icons.Filled.Delete,
title = stringResource(id = R.string.settings_uninstall),
onClick = {
uninstallDialog.show()
}
)
}
enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) {
TEMPORARY(
R.string.settings_uninstall_temporary,
R.string.settings_uninstall_temporary_message,
Icons.Filled.Delete
),
PERMANENT(
R.string.settings_uninstall_permanent,
R.string.settings_uninstall_permanent_message,
Icons.Filled.DeleteForever
),
RESTORE_STOCK_IMAGE(
R.string.settings_restore_stock_image,
R.string.settings_restore_stock_image_message,
Icons.AutoMirrored.Filled.Undo
),
NONE(0, 0, Icons.Filled.Delete)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
return rememberCustomDialog { dismiss ->
val options = listOf(
// UninstallType.TEMPORARY,
UninstallType.PERMANENT,
UninstallType.RESTORE_STOCK_IMAGE
)
val listOptions = options.map {
ListOption(
titleText = stringResource(it.title),
subtitleText = if (it.message != 0) stringResource(it.message) else null,
icon = IconSource(it.icon)
)
}
var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy(
surface = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
AlertDialog(
onDismissRequest = {
dismiss()
},
title = {
Text(
text = stringResource(R.string.settings_uninstall),
style = MaterialTheme.typography.headlineSmall,
)
},
text = {
Column(
modifier = Modifier.padding(vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
options.forEachIndexed { index, option ->
val isSelected = selectedOption == option
val backgroundColor = if (isSelected)
MaterialTheme.colorScheme.primaryContainer
else
Color.Transparent
val contentColor = if (isSelected)
MaterialTheme.colorScheme.onPrimaryContainer
else
MaterialTheme.colorScheme.onSurface
Row(
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(backgroundColor)
.clickable {
selectedOption = option
}
.padding(vertical = 12.dp, horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = option.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = listOptions[index].titleText,
style = MaterialTheme.typography.titleMedium,
)
listOptions[index].subtitleText?.let {
Text(
text = it,
style = MaterialTheme.typography.bodyMedium,
color = if (isSelected)
contentColor.copy(alpha = 0.8f)
else
MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (isSelected) {
Icon(
imageVector = Icons.Default.RadioButtonChecked,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
} else {
Icon(
imageVector = Icons.Default.RadioButtonUnchecked,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(24.dp)
)
}
}
}
}
},
confirmButton = {
Button(
onClick = {
selectedOption?.let { onSelected(it) }
dismiss()
},
enabled = selectedOption != null,
) {
Text(
text = stringResource(android.R.string.ok)
)
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
}
) {
Text(
text = stringResource(android.R.string.cancel),
)
}
},
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
TopAppBar(
title = {
Text(
text = stringResource(R.string.settings),
style = MaterialTheme.typography.titleLarge
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}

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