205 Commits

Author SHA1 Message Date
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
171 changed files with 26800 additions and 6981 deletions

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

@@ -24,23 +24,23 @@ jobs:
matrix: matrix:
include: include:
- version: "android12-5.10" - version: "android12-5.10"
sub_level: 233 sub_level: 237
os_patch_level: 2025-02 os_patch_level: 2025-06
- version: "android13-5.10" - version: "android13-5.10"
sub_level: 234 sub_level: 236
os_patch_level: 2025-03 os_patch_level: 2025-05
- version: "android13-5.15" - version: "android13-5.15"
sub_level: 178 sub_level: 180
os_patch_level: 2025-03 os_patch_level: 2025-05
- version: "android14-5.15" - version: "android14-5.15"
sub_level: 178 sub_level: 180
os_patch_level: 2025-03 os_patch_level: 2025-05
- version: "android14-6.1" - version: "android14-6.1"
sub_level: 129 sub_level: 138
os_patch_level: 2025-04 os_patch_level: 2025-06
- version: "android15-6.6" - version: "android15-6.6"
sub_level: 82 sub_level: 89
os_patch_level: 2025-04 os_patch_level: 2025-06
# uses: ./.github/workflows/gki-kernel-mock.yml when debugging # uses: ./.github/workflows/gki-kernel-mock.yml when debugging
uses: ./.github/workflows/gki-kernel.yml uses: ./.github/workflows/gki-kernel.yml
with: with:

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

@@ -119,6 +119,8 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
- target: x86_64-linux-android - target: x86_64-linux-android
os: ubuntu-latest os: ubuntu-latest
- target: armv7-linux-androideabi
os: ubuntu-latest
uses: ./.github/workflows/ksud.yml uses: ./.github/workflows/ksud.yml
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}
@@ -198,12 +200,20 @@ jobs:
name: ksud-x86_64-linux-android name: ksud-x86_64-linux-android
path: . path: .
- name: Download arm ksud
uses: actions/download-artifact@v4
with:
name: ksud-armv7-linux-androideabi
path: .
- name: Copy ksud to app jniLibs - name: Copy ksud to app jniLibs
run: | run: |
mkdir -p app/src/main/jniLibs/arm64-v8a mkdir -p app/src/main/jniLibs/arm64-v8a
mkdir -p app/src/main/jniLibs/x86_64 mkdir -p app/src/main/jniLibs/x86_64
mkdir -p app/src/main/jniLibs/armeabi-v7a
cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so
cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so
cp -f ../armv7-linux-androideabi/release/zakozako ../manager/app/src/main/jniLibs/armeabi-v7a/libzakozako.so
- name: Copy kpmmgr to app jniLibs - name: Copy kpmmgr to app jniLibs
run: | run: |

40
.github/workflows/crowdin.yml vendored Normal file
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

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,6 +1,6 @@
# SukiSU Ultra # SukiSU Ultra
**English** | [简体中文](README.md) | [日本語](README-ja.md) **English** | [简体中文](README.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU) Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU)
@@ -10,67 +10,102 @@ Android device root solution based on [KernelSU](https://github.com/tiann/Kernel
> >
> However, we will be a separately maintained branch of KSU in the future > However, we will be a separately maintained branch of KSU in the future
- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only)
## How to add ## How to add
Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices) Using main branching (non-GKI device builds are not supported) (requires manual integration of susfs)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
``` ```
Use the main branch Using branches that support non-GKI devices (requires manual integration of susfs)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
``` ```
## How to use integrated susfs ## How to use integrated susfs
1. Use the susfs-dev branch directly without any patching 1. Use susfs-main or other susfs-\* branches directly, no need to integrate susfs again (supports non-GKI device builds)
## KPM support ```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
```
- We have removed duplicate KSU functions based on KernelPatch and retained KPM support. ## Hook method
- This method references the hook [method by rsuntk](https://github.com/rsuntk/KernelSU)
1. **KPROBES hook:**
- Also used for Loadable Kernel Module (LKM)
- Default hook method on GKI kernels.
- Need `CONFIG_KPROBES=y`
2. **Manual hook:**
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
- Default hook method on Non-GKI kernels.
- Need `CONFIG_KSU_MANUAL_HOOK=y`
## KPM Support
- Based on KernelPatch, we have removed duplicates of KSU and kept only KPM support.
- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality. - We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality.
Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch Repository address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch
KPM template address: https://github.com/udochina/KPM-Build-Anywhere KPM templates: https://github.com/udochina/KPM-Build-Anywhere
> [!Note]
>
> 1. `CONFIG_KPM=y` needs to be added.
> 2. Non-GKI devices need to add `CONFIG_KALLSYMS=y` and `CONFIG_KALLSYMS_ALL=y` as well.
> 3. Some kernel source code below `4.19` also needs to be backport from `4.19` to the header file `set_memory.h`.
## How to do a system update to retain ROOT
- After OTA, don't reboot first, go to the manager flashing/patching kernel interface, find `GKI/non_GKI install` and select the Anykernel3 kernel zip file that needs to be flashed, select the slot that is opposite to the current running slot of the system for flashing, and then reboot to retain the GKI mode update This method is not supported for all non-GKI devices, so please try it yourself. It is the safest way to use TWRP for non-GKI devices.
- Or use LKM mode to install to the unused slot (after OTA).
## Compatibility Status
- KernelSU (versions prior to v1.0.0) officially supports Android GKI 2.0 devices (kernel 5.10+)
- Older kernels (4.4+) are also compatible, but the kernel must be built manually
- KernelSU can support 3.x kernels (3.4-3.18) through additional reverse ports
- Currently supports `arm64-v8a`, `armeabi-v7a (bare)` and some `X86_64`
## More links ## More links
**If you need to submit a translation for the manager go to** https://crowdin.com/project/SukiSU-Ultra
Projects compiled based on Sukisu and susfs Projects compiled based on Sukisu and susfs
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [More patched GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) including ZRAM patches, KPM, susfs...
- [Less patched GKI](https://github.com/MiRinFork/GKI_SukiSU_SUSFS/releases) only susfs
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## Hook method
- This method references the hook method from (https://github.com/rsuntk/KernelSU)
1. **KPROBES hook:**
- Also used for Loadable Kernel Module (LKM)
- Default hook method on GKI kernels.
- Need `CONFIG_KPROBES=y`
2. **Manual hook:**
- Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5
- Default hook method on Non-GKI kernels.
- Need `CONFIG_KSU_MANUAL_HOOK=y`
## Usage ## Usage
### GKI ### Universal GKI
Please follow this guide. Please **all** refer to https://kernelsu.org/zh_CN/guide/installation.html
https://kernelsu.org/guide/installation.html
> [!Note]
>
> 1. for devices with GKI 2.0 such as Xiaomi, Redmi, Samsung, etc. (excludes kernel-modified manufacturers such as Meizu, OnePlus, Zenith, and oppo)
> 2. Find the GKI build in [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5). Find the device kernel version. Then download it and use TWRP or kernel flashing tool to flash the zip file with AnyKernel3 suffix. Pixel user need use _Less patched GKI_.
> 3. The .zip archive without suffix is uncompressed, the gz suffix is the compression used by Tenguet models.
### OnePlus ### OnePlus
1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix. 1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix.
> [!Note] > [!Note]
>
> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6. > - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6.
> - Please search for the processor codename by yourself, usually it is all English without numbers. > - Please search for the processor codename by yourself, usually it is all English without numbers.
> - You can find the branch and configuration files from the OnePlus open-source kernel repository. > - You can find the branch and configuration files from the OnePlus open-source kernel repository.
@@ -79,15 +114,25 @@ https://kernelsu.org/guide/installation.html
1. Kernel-based `su` and root access management. 1. Kernel-based `su` and root access management.
2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system, but based on [Magic Mount](https://github.com/5ec1cff/KernelSU) from 5ec1cff 2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system, but based on [Magic Mount](https://github.com/5ec1cff/KernelSU) from 5ec1cff
3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage. 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage.
4. Bringing back non-GKI/GKI 1.0 support 4. Bringing back non-GKI/GKI 1.0 support
5. More customization 5. More customization
6. Support for KPM kernel modules 6. Support for KPM kernel modules
## Troubleshooting
1. Uninstalling the KernelSU Manager device is stuck. → Uninstall the application with package name com.sony.playmemories.mobile.
## License ## License
- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license. - The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license.
- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
- The images of the files `ic_launcher(?!.*alt.*).*` with anime character emoticons are copyrighted by [五十根大虾仁](https://space.bilibili.com/370927), the Brand Intellectual Property in the images is owned by [明风 OuO](https://space.bilibili.com/274939213), and the vectorization is done by @MiRinChan. Before using these files, in addition to complying with [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt), you also need to comply with the authorization of the two authors to use these artistic contents.
- Except for the files or directories mentioned above, all other parts are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license.
## Afdian link
- https://afdian.com/a/shirkneko
## Sponsorship list ## Sponsorship list
@@ -96,8 +141,8 @@ https://kernelsu.org/guide/installation.html
- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project - [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project
- [yspbwx2010](https://github.com/yspbwx2010) Many thanks - [yspbwx2010](https://github.com/yspbwx2010) Many thanks
- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao - [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao
- [Saksham Singla](https://github.com/TypeFlu) Website provision as well as maintenance
If the above list does not have your name, I will update it as soon as possible, and thanks again for your support! - [OukaroMF](https://github.com/OukaroMF) Donation of website domain name
## Contributions ## Contributions

View File

@@ -1,6 +1,6 @@
# SukiSU Ultra # SukiSU Ultra
**日本語** | [简体中文](README.md) | [English](README-en.md) **日本語** | [简体中文](README.md) | [English](README-en.md) | [Türkçe](README-tr.md)
[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション [KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション
@@ -11,22 +11,25 @@
> >
> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。 > ただし、将来的には KSU とは別に管理されるブランチとなる予定です。
- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ)
## 追加方法 ## 追加方法
susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。 メイン分岐の使用(GKI デバイス以外のビルドはサポートされていません。) (手動によるサスフ統合が必要)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
``` ```
メインブランチを使用する場合 GKI以外のデバイスをサポートするブランチを使用する (手動によるサスフ統合が必要)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
``` ```
## 統合された susfs の使い方 ## 統合された susfs の使い方
1. パッチを当てずに susfs-dev ブランチを直接使用してください。 1. susfs-mainまたは他のsusfs-*ブランチを直接使用し、susfsを再度統合する必要はありません
```
curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-main
```
## KPM に対応 ## KPM に対応
@@ -37,9 +40,28 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setu
KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere
> [!Note]
> 1. `CONFIG_KPM=y` が必要である。
> 2.非 GKI デバイスには `CONFIG_KALLSYMS=y` と `CONFIG_KALLSYMS_ALL=y` も必要です。
> 3.いくつかのカーネル `4.19` およびそれ以降のソースコードでは、 `4.19` からバックポートされた `set_memory.h` ヘッダーファイルも必要です。
## ROOT を保持するシステムアップデートの方法
- OTAの後、最初に再起動せず、マネージャのフラッシュ/パッチカーネルインターフェイスに移動し、`GKI/non_GKI 取り付け`を見つけ、フラッシュする必要があるAnykernel3カーネルzipファイルを選択し、フラッシュするためにシステムの現在の実行スロットと反対のスロットを選択し、GKIモードアップデートを保持するために再起動しますこの方法は、現時点ではすべてのnon_GKIデバイスでサポートされていませんので、各自でお試しください。 (この方法は、すべての非GKIデバイスでサポートされていませんので、ご自身でお試しください)。
- または、LKMモードを使用して未使用のスロットにインストールします(OTA後)。
## 互換性ステータス
- KernelSUv1.0.0より前のバージョンはAndroid GKI 2.0デバイスカーネル5.10以上)を公式にサポートしています。
- 古いカーネル4.4+)も互換性がありますが、カーネルは手動でビルドする必要があります。
- KernelSU は追加のリバースポートを通じて 3.x カーネル (3.4-3.18) をサポートしています。
- 現在は `arm64-v8a``armeabi-v7a (bare)`、いくつかの `X86_64` をサポートしています。
## その他のリンク ## その他のリンク
SukiSU と susfs をベースにコンパイルされたプロジェクトです **監督に翻訳を提出する必要がある場合は、https://crowdin.com/project/SukiSU-Ultra
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) - [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
@@ -61,12 +83,14 @@ SukiSU と susfs をベースにコンパイルされたプロジェクトです
## 使い方 ## 使い方
### GKI ### ユニバーサルGKI
このガイドに従ってください。 https://kernelsu.org/zh_CN/guide/installation.html をご参照ください。
https://kernelsu.org/ja_JP/guide/installation.html
> [!Note]
> 1.Xiaomi、Redmi、Samsung などの GKI 2.0 を搭載したデバイス用 (Meizu、Yiga、Zenith、oppo などのマジックカーネルを搭載したメーカーは除く)。
> 2. [more links](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5) で GKI ビルドを検索します。 デバイスのカーネルバージョンを検索します。 次に、それをダウンロードし、TWRPまたはカーネルフラッシングツールを使用して、AnyKernel3の接尾辞が付いたzipファイルをフラッシュします。
> 接尾辞なしの.zipアーカイブは非圧縮で、接尾辞gzはTenguetモデルで使用されている圧縮方法です。
### OnePlus ### OnePlus
1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。 1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。
@@ -85,10 +109,15 @@ https://kernelsu.org/ja_JP/guide/installation.html
5. その他のカスタマイズ 5. その他のカスタマイズ
6. KPM カーネルモジュールに対応 6. KPM カーネルモジュールに対応
## トラブルシューティング
1.KernelSU Managerのアンインストールができない。 →パッケージ名com.sony.playmemories.mobile.KernelSU Managerのアプリケーションをアンインストールする。
## ライセンス ## ライセンス
- kernel ディレクトリのファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。 - `kernel` ディレクトリ以下のファイルは[GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)す。
- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。 - アニメキャラクターの絵文字を含むファイル `ic_launcher(?!.*alt.*).*` の画像は[五十根大虾仁](https://space.bilibili.com/370927)が著作権を所有しており、画像内のブランド知的財産権は[明风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)です。
## スポンサーシップの一覧 ## スポンサーシップの一覧
@@ -97,8 +126,8 @@ https://kernelsu.org/ja_JP/guide/installation.html
- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。 - [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。
- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。 - [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。
- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう - [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう
- [Saksham Singla](https://github.com/TypeFlu) ウェブサイトの提供およびメンテナンス
上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。 - [OukaroMF](https://github.com/OukaroMF) ウェブサイトドメイン名の寄付
## 貢献者 ## 貢献者

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

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

View File

@@ -1,6 +1,6 @@
# SukiSU Ultra # SukiSU Ultra
**简体中文** | [English](README-en.md) | [日本語](README-ja.md) **简体中文** | [English](README-en.md) | [日本語](README-ja.md) | [Türkçe](README-tr.md)
基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案 基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案
@@ -14,34 +14,41 @@
在内核源码的根目录下执行以下命令: 在内核源码的根目录下执行以下命令:
使用 susfs-dev 分支(已集成 susfs带非 GKI 设备的支持) 使用 main 分支 (不支持非 GKI 设备构建) (需要手动集成 susfs)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main
``` ```
使用 main 分支 使用支持非 GKI 设备的分支 (需要手动集成 susfs)
``` ```
curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main curl -LSs "https://raw.githubusercontent.com/SukiSU-Ultra/SukiSU-Ultra/main/kernel/setup.sh" | bash -s nongki
``` ```
## 如何集成 susfs ## 如何集成 susfs
1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 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
```
## 钩子方法 ## 钩子方法
- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU) - 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU)
1. **KPROBES 钩子:** 1. **KPROBES 钩子:**
- 用于可加载内核模块 (LKM)
- GKI 2.0 内核的默认钩子方法 - 用于可加载内核模块 (LKM)
- 需要 `CONFIG_KPROBES=y` - GKI 2.0 内核的默认钩子方法
- 需要 `CONFIG_KPROBES=y`
2. **手动钩子:** 2. **手动钩子:**
- 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - 标准的 KernelSU 钩子https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source
- backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5 - backslashxx 的 syscall 手动钩子https://github.com/backslashxx/KernelSU/issues/5
- 非 GKI 内核的默认挂钩方法 - 非 GKI 内核的默认挂钩方法
- 需要 `CONFIG_KSU_MANUAL_HOOK=y` - 需要 `CONFIG_KSU_MANUAL_HOOK=y`
## KPM 支持 ## KPM 支持
@@ -52,10 +59,35 @@ curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/
KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
> [!Note]
>
> 1. 需要 `CONFIG_KPM=y`
> 2. 非 GKI 设备还需要 `CONFIG_KALLSYMS=y` 和 `CONFIG_KALLSYMS_ALL=y`
> 3. 部分内核 `4.19` 以下源码还需要从 `4.19` 向后移植头文件 `set_memory.h`
## 如何进行系统更新保留 ROOT
- OTA 后先不要重启,进入管理器刷写/修补内核界面,找到 `GKI/non_GKI安装` 选择需要刷写的 Anykernel3 内核压缩文件,选择与现在系统运行槽位相反的槽位进行刷写并重启即可保留 GKI 模式更新(暂不支持所有非 GKI 设备使用这种方法,请自行尝试。非 GKI 设备使用 TWRP 刷写是最稳妥的)
- 或者使用 LKM 模式的安装到未使用的槽位OTA 后)
## 兼容状态
- KernelSUv1.0.0 之前版本)正式支持 Android GKI 2.0 设备(内核 5.10+
- 旧内核4.4+)也兼容,但必须手动构建内核
- 通过更多的反向移植KernelSU 可以支持 3.x 内核3.4-3.18
- 目前支持 `arm64-v8a` `armeabi-v7a (bare)` 和部分 `X86_64`
## 更多链接 ## 更多链接
**如果你需要为管理器提交翻译请前往** https://crowdin.com/project/SukiSU-Ultra
基于 SukiSU 和 susfs 编译的项目 基于 SukiSU 和 susfs 编译的项目
- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)
- [增强 GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS)(包括 ZRAM 算法等补丁、KPM、susfs 等)
- [GKI](https://github.com/MiRinFork/GKI_SukiSU_SUSFS/releases)(若增强 GKI boot 失败再尝试这份,这份没有 KPM 等修改,只有 susfs
- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS)
## 使用方法 ## 使用方法
@@ -65,16 +97,17 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html 请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html
> [!Note] > [!Note]
>
> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo) > 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo)
> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可 > 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用 TWRP 或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可。Pixel 请使用不是增强的 GKI。
> 3. 一般不带后缀的 .zip 压缩包是未压缩的gz 后缀的为天玑机型所使用的压缩方式 > 3. 一般不带后缀的 .zip 压缩包是未压缩的gz 后缀的为天玑机型所使用的压缩方式
### 一加 ### 一加
1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可 1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可
> [!Note] > [!Note]
>
> - 内核版本只需要填写前两位即可,如 5.105.156.16.6 > - 内核版本只需要填写前两位即可,如 5.105.156.16.6
> - 处理器代号请自行搜索,一般为全英文不带数字的代号 > - 处理器代号请自行搜索,一般为全英文不带数字的代号
> - 分支和配置文件请自行到一加内核开源地址进行填写 > - 分支和配置文件请自行到一加内核开源地址进行填写
@@ -88,10 +121,19 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
5. 更多自定义功能 5. 更多自定义功能
6. 对 KPM 内核模块的支持 6. 对 KPM 内核模块的支持
## 疑难解答
1. 卸载 KernelSU 管理器设备卡死。→ 卸载包名为 com.sony.playmemories.mobile 的应用。
## 许可证 ## 许可证
- `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。 - `kernel` 目录下的文件是 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。
- `kernel` 目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html) - 有动漫人物图片表情包的这些文件 `ic_launcher(?!.*alt.*).*` 的图像版权为[五十根大虾仁](https://space.bilibili.com/370927)所有,图像中的 Brand Intellectual Property 由[明风 OuO](https://space.bilibili.com/274939213)所有,矢量化由 @MiRinChan 完成,在使用这些文件之前,除了必须遵守 [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt) 以外,还需要遵守向前两者索要使用这些艺术内容的授权
- 除了以上所述的文件或目录外,所有其他部分均为 [GPL-3.0 或更高版本](https://www.gnu.org/licenses/gpl-3.0.html)。
## 爱发电链接
- https://afdian.com/a/shirkneko
## 赞助名单 ## 赞助名单
@@ -100,17 +142,17 @@ KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere
- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持 - [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持
- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢 - [yspbwx2010](https://github.com/yspbwx2010) 非常感谢
- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT - [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT
- [Saksham Singla](https://github.com/TypeFlu) 网站的提供以及维护
如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持 - [OukaroMF](https://github.com/OukaroMF) 网站域名捐赠
## 贡献 ## 贡献
- [KernelSU](https://github.com/tiann/KernelSU):原始项目 - [KernelSU](https://github.com/tiann/KernelSU):原始项目
- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目 - [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目
- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非GKI设备重新进行支持 - [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非 GKI 设备重新进行支持
- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统 - [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统
- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/)KernelSU 的构想 - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/)KernelSU 的构想
- [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具 - [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具
- [genuine](https://github.com/brevent/genuine/)APK v2 签名验证 - [genuine](https://github.com/brevent/genuine/)APK v2 签名验证
- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能 - [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技能
- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分 - [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分

View File

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

View File

@@ -19,19 +19,33 @@ obj-$(CONFIG_KSU) += kernelsu.o
obj-$(CONFIG_KPM) += kpm/ obj-$(CONFIG_KPM) += kpm/
# .git is a text file while the module is imported by 'git submodule add'. REPO_OWNER := SukiSU-Ultra
ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0) REPO_NAME := SukiSU-Ultra
$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow) REPO_BRANCH := main
KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count main)
# ksu_version: major * 10000 + git version + 606 for historical reasons GIT_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git
$(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 606)) CURL_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin curl
$(info -- KernelSU version: $(KSU_VERSION))
ccflags-y += -DKSU_VERSION=$(KSU_VERSION) KSU_GITHUB_VERSION := $(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')
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!") ifeq ($(KSU_GITHUB_VERSION),)
ccflags-y += -DKSU_VERSION=16 ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0)
$(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow)
KSU_LOCAL_VERSION := $(shell cd $(srctree)/$(src); $(GIT_BIN) rev-list --count HEAD)
$(eval KSU_VERSION := $(shell expr 10000 + $(KSU_LOCAL_VERSION) + 700))
$(info -- SukiSU-Ultra version (local .git): $(KSU_VERSION))
else
$(eval KSU_VERSION := 13000)
$(warning -- Could not fetch version online or via local .git! Using fallback version: $(KSU_VERSION))
endif
else
$(eval KSU_VERSION := $(shell expr 10000 + $(KSU_GITHUB_VERSION) + 700))
$(info -- SukiSU-Ultra version (GitHub): $(KSU_VERSION))
endif endif
$(info -- SukiSU-Ultra version: $(KSU_VERSION))
ccflags-y += -DKSU_VERSION=$(KSU_VERSION)
ifndef KSU_EXPECTED_SIZE ifndef KSU_EXPECTED_SIZE
KSU_EXPECTED_SIZE := 0x35c KSU_EXPECTED_SIZE := 0x35c
endif endif

View File

@@ -110,6 +110,7 @@ static void setup_groups(struct root_profile *profile, struct cred *cred)
groups_sort(group_info); groups_sort(group_info);
set_groups(cred, group_info); set_groups(cred, group_info);
put_group_info(group_info);
} }
static void disable_seccomp() static void disable_seccomp()
@@ -134,18 +135,18 @@ void escape_to_root(void)
{ {
struct cred *cred; struct cred *cred;
rcu_read_lock(); cred = prepare_creds();
if (!cred) {
do { pr_warn("prepare_creds failed!\n");
cred = (struct cred *)__task_cred((current)); return;
BUG_ON(!cred); }
} while (!get_cred_rcu(cred));
if (cred->euid.val == 0) { if (cred->euid.val == 0) {
pr_warn("Already root, don't escape!\n"); pr_warn("Already root, don't escape!\n");
rcu_read_unlock(); abort_creds(cred);
return; return;
} }
struct root_profile *profile = ksu_get_root_profile(cred->uid.val); struct root_profile *profile = ksu_get_root_profile(cred->uid.val);
cred->uid.val = profile->uid; cred->uid.val = profile->uid;
@@ -176,7 +177,7 @@ void escape_to_root(void)
setup_groups(profile, cred); setup_groups(profile, cred);
rcu_read_unlock(); commit_creds(cred);
// Refer to kernel/seccomp.c: seccomp_set_mode_strict // Refer to kernel/seccomp.c: seccomp_set_mode_strict
// When disabling Seccomp, ensure that current->sighand->siglock is held during the operation. // When disabling Seccomp, ensure that current->sighand->siglock is held during the operation.
@@ -226,6 +227,7 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry)
return 0; return 0;
} }
#ifdef CONFIG_EXT4_FS
static void nuke_ext4_sysfs() { static void nuke_ext4_sysfs() {
struct path path; struct path path;
int err = kern_path("/data/adb/modules", 0, &path); int err = kern_path("/data/adb/modules", 0, &path);
@@ -242,7 +244,11 @@ static void nuke_ext4_sysfs() {
} }
ext4_unregister_sysfs(sb); ext4_unregister_sysfs(sb);
path_put(&path);
} }
#else
static inline void nuke_ext4_sysfs() { }
#endif
int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
unsigned long arg4, unsigned long arg5) unsigned long arg4, unsigned long arg5)
@@ -425,6 +431,13 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3,
} }
#endif #endif
if (arg2 == CMD_ENABLE_KPM) {
bool KPM_Enabled = IS_ENABLED(CONFIG_KPM);
if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled)))
pr_info("KPM: copy_to_user() failed\n");
return 0;
}
// all other cmds are for 'root manager' // all other cmds are for 'root manager'
if (!from_manager) { if (!from_manager) {
return 0; return 0;
@@ -553,11 +566,13 @@ static void try_umount(const char *mnt, bool check_mnt, int flags)
if (path.dentry != path.mnt->mnt_root) { if (path.dentry != path.mnt->mnt_root) {
// it is not root mountpoint, maybe umounted by others already. // it is not root mountpoint, maybe umounted by others already.
path_put(&path);
return; return;
} }
// we are only interest in some specific mounts // we are only interest in some specific mounts
if (check_mnt && !should_umount(&path)) { if (check_mnt && !should_umount(&path)) {
path_put(&path);
return; return;
} }

View File

@@ -60,6 +60,13 @@ uid_t sukisu_get_manager_uid() {
return ksu_manager_uid; return ksu_manager_uid;
} }
static
void sukisu_set_manager_uid(uid_t uid, int force) {
if(force || ksu_manager_uid == -1) {
ksu_manager_uid = uid;
}
}
// ====================================================================== // ======================================================================
struct CompactAddressSymbol { struct CompactAddressSymbol {
@@ -75,7 +82,8 @@ static struct CompactAddressSymbol address_symbol [] = {
{ "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude }, { "get_ap_mod_exclude", &sukisu_get_ap_mod_exclude },
{ "is_uid_should_umount", &sukisu_is_uid_should_umount }, { "is_uid_should_umount", &sukisu_is_uid_should_umount },
{ "is_current_uid_manager", &sukisu_is_current_uid_manager }, { "is_current_uid_manager", &sukisu_is_current_uid_manager },
{ "get_manager_uid", &sukisu_get_manager_uid } { "get_manager_uid", &sukisu_get_manager_uid },
{ "sukisu_set_manager_uid", &sukisu_set_manager_uid }
}; };
unsigned long sukisu_compact_find_symbol(const char* name) { unsigned long sukisu_compact_find_symbol(const char* name) {

View File

@@ -167,7 +167,11 @@ DYNAMIC_STRUCT_BEGIN(task_struct)
DEFINE_MEMBER(task_struct, group_leader) DEFINE_MEMBER(task_struct, group_leader)
DEFINE_MEMBER(task_struct, mm) DEFINE_MEMBER(task_struct, mm)
DEFINE_MEMBER(task_struct, active_mm) DEFINE_MEMBER(task_struct, active_mm)
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 19, 0)
DEFINE_MEMBER(task_struct, pids[PIDTYPE_PID].pid)
#else
DEFINE_MEMBER(task_struct, thread_pid) DEFINE_MEMBER(task_struct, thread_pid)
#endif
DEFINE_MEMBER(task_struct, files) DEFINE_MEMBER(task_struct, files)
DEFINE_MEMBER(task_struct, seccomp) DEFINE_MEMBER(task_struct, seccomp)
#ifdef CONFIG_THREAD_INFO_IN_TASK #ifdef CONFIG_THREAD_INFO_IN_TASK

View File

@@ -23,6 +23,7 @@
#define CMD_UID_SHOULD_UMOUNT 13 #define CMD_UID_SHOULD_UMOUNT 13
#define CMD_IS_SU_ENABLED 14 #define CMD_IS_SU_ENABLED 14
#define CMD_ENABLE_SU 15 #define CMD_ENABLE_SU 15
#define CMD_ENABLE_KPM 100
#define EVENT_POST_FS_DATA 1 #define EVENT_POST_FS_DATA 1
#define EVENT_BOOT_COMPLETED 2 #define EVENT_BOOT_COMPLETED 2

View File

@@ -63,6 +63,10 @@ u32 ksu_devpts_sid;
// Detect whether it is on or not // Detect whether it is on or not
static bool is_boot_phase = true; static bool is_boot_phase = true;
#ifdef CONFIG_COMPAT
bool ksu_is_compat __read_mostly = false;
#endif
void on_post_fs_data(void) void on_post_fs_data(void)
{ {
static bool done = false; static bool done = false;
@@ -107,6 +111,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)
if (get_user(compat, argv.ptr.compat + nr)) if (get_user(compat, argv.ptr.compat + nr))
return ERR_PTR(-EFAULT); return ERR_PTR(-EFAULT);
ksu_is_compat = true;
return compat_ptr(compat); return compat_ptr(compat);
} }
#endif #endif

View File

@@ -137,17 +137,45 @@ void apply_kernelsu_rules()
#define CMD_TYPE_CHANGE 8 #define CMD_TYPE_CHANGE 8
#define CMD_GENFSCON 9 #define CMD_GENFSCON 9
#ifdef CONFIG_64BIT
struct sepol_data { struct sepol_data {
u32 cmd; u32 cmd;
u32 subcmd; u32 subcmd;
char __user *sepol1; u64 field_sepol1;
char __user *sepol2; u64 field_sepol2;
char __user *sepol3; u64 field_sepol3;
char __user *sepol4; u64 field_sepol4;
char __user *sepol5; u64 field_sepol5;
char __user *sepol6; u64 field_sepol6;
char __user *sepol7; u64 field_sepol7;
}; };
#ifdef CONFIG_COMPAT
extern bool ksu_is_compat __read_mostly;
struct sepol_compat_data {
u32 cmd;
u32 subcmd;
u32 field_sepol1;
u32 field_sepol2;
u32 field_sepol3;
u32 field_sepol4;
u32 field_sepol5;
u32 field_sepol6;
u32 field_sepol7;
};
#endif // CONFIG_COMPAT
#else
struct sepol_data {
u32 cmd;
u32 subcmd;
u32 field_sepol1;
u32 field_sepol2;
u32 field_sepol3;
u32 field_sepol4;
u32 field_sepol5;
u32 field_sepol6;
u32 field_sepol7;
};
#endif // CONFIG_64BIT
static int get_object(char *buf, char __user *user_object, size_t buf_sz, static int get_object(char *buf, char __user *user_object, size_t buf_sz,
char **object) char **object)
@@ -191,15 +219,59 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
if (!getenforce()) { if (!getenforce()) {
pr_info("SELinux permissive or disabled when handle policy!\n"); pr_info("SELinux permissive or disabled when handle policy!\n");
} }
u32 cmd, subcmd;
char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7;
#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT)
if (unlikely(ksu_is_compat)) {
struct sepol_compat_data compat_data;
if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) {
pr_err("sepol: copy sepol_data failed.\n");
return -1;
}
sepol1 = compat_ptr(compat_data.field_sepol1);
sepol2 = compat_ptr(compat_data.field_sepol2);
sepol3 = compat_ptr(compat_data.field_sepol3);
sepol4 = compat_ptr(compat_data.field_sepol4);
sepol5 = compat_ptr(compat_data.field_sepol5);
sepol6 = compat_ptr(compat_data.field_sepol6);
sepol7 = compat_ptr(compat_data.field_sepol7);
cmd = compat_data.cmd;
subcmd = compat_data.subcmd;
} else {
struct sepol_data data;
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
pr_err("sepol: copy sepol_data failed.\n");
return -1;
}
sepol1 = data.field_sepol1;
sepol2 = data.field_sepol2;
sepol3 = data.field_sepol3;
sepol4 = data.field_sepol4;
sepol5 = data.field_sepol5;
sepol6 = data.field_sepol6;
sepol7 = data.field_sepol7;
cmd = data.cmd;
subcmd = data.subcmd;
}
#else
// basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n)
struct sepol_data data; struct sepol_data data;
if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) {
pr_err("sepol: copy sepol_data failed.\n"); pr_err("sepol: copy sepol_data failed.\n");
return -1; return -1;
} }
sepol1 = data.field_sepol1;
u32 cmd = data.cmd; sepol2 = data.field_sepol2;
u32 subcmd = data.subcmd; sepol3 = data.field_sepol3;
sepol4 = data.field_sepol4;
sepol5 = data.field_sepol5;
sepol6 = data.field_sepol6;
sepol7 = data.field_sepol7;
cmd = data.cmd;
subcmd = data.subcmd;
#endif
rcu_read_lock(); rcu_read_lock();
@@ -213,22 +285,22 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char perm_buf[MAX_SEPOL_LEN]; char perm_buf[MAX_SEPOL_LEN];
char *s, *t, *c, *p; char *s, *t, *c, *p;
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) { if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
pr_err("sepol: copy src failed.\n"); pr_err("sepol: copy src failed.\n");
goto exit; goto exit;
} }
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) { if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
pr_err("sepol: copy tgt failed.\n"); pr_err("sepol: copy tgt failed.\n");
goto exit; goto exit;
} }
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) { if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
pr_err("sepol: copy cls failed.\n"); pr_err("sepol: copy cls failed.\n");
goto exit; goto exit;
} }
if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) < if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) <
0) { 0) {
pr_err("sepol: copy perm failed.\n"); pr_err("sepol: copy perm failed.\n");
goto exit; goto exit;
@@ -258,24 +330,24 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char perm_set[MAX_SEPOL_LEN]; char perm_set[MAX_SEPOL_LEN];
char *s, *t, *c; char *s, *t, *c;
if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) { if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) {
pr_err("sepol: copy src failed.\n"); pr_err("sepol: copy src failed.\n");
goto exit; goto exit;
} }
if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) { if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) {
pr_err("sepol: copy tgt failed.\n"); pr_err("sepol: copy tgt failed.\n");
goto exit; goto exit;
} }
if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) { if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) {
pr_err("sepol: copy cls failed.\n"); pr_err("sepol: copy cls failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(operation, data.sepol4, if (strncpy_from_user(operation, sepol4,
sizeof(operation)) < 0) { sizeof(operation)) < 0) {
pr_err("sepol: copy operation failed.\n"); pr_err("sepol: copy operation failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) < if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) <
0) { 0) {
pr_err("sepol: copy perm_set failed.\n"); pr_err("sepol: copy perm_set failed.\n");
goto exit; goto exit;
@@ -295,7 +367,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
} else if (cmd == CMD_TYPE_STATE) { } else if (cmd == CMD_TYPE_STATE) {
char src[MAX_SEPOL_LEN]; char src[MAX_SEPOL_LEN];
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
pr_err("sepol: copy src failed.\n"); pr_err("sepol: copy src failed.\n");
goto exit; goto exit;
} }
@@ -315,11 +387,11 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char type[MAX_SEPOL_LEN]; char type[MAX_SEPOL_LEN];
char attr[MAX_SEPOL_LEN]; char attr[MAX_SEPOL_LEN];
if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) { if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) {
pr_err("sepol: copy type failed.\n"); pr_err("sepol: copy type failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) { if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) {
pr_err("sepol: copy attr failed.\n"); pr_err("sepol: copy attr failed.\n");
goto exit; goto exit;
} }
@@ -339,7 +411,7 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
} else if (cmd == CMD_ATTR) { } else if (cmd == CMD_ATTR) {
char attr[MAX_SEPOL_LEN]; char attr[MAX_SEPOL_LEN];
if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) { if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) {
pr_err("sepol: copy attr failed.\n"); pr_err("sepol: copy attr failed.\n");
goto exit; goto exit;
} }
@@ -356,28 +428,28 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char default_type[MAX_SEPOL_LEN]; char default_type[MAX_SEPOL_LEN];
char object[MAX_SEPOL_LEN]; char object[MAX_SEPOL_LEN];
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
pr_err("sepol: copy src failed.\n"); pr_err("sepol: copy src failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) { if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
pr_err("sepol: copy tgt failed.\n"); pr_err("sepol: copy tgt failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) { if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
pr_err("sepol: copy cls failed.\n"); pr_err("sepol: copy cls failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(default_type, data.sepol4, if (strncpy_from_user(default_type, sepol4,
sizeof(default_type)) < 0) { sizeof(default_type)) < 0) {
pr_err("sepol: copy default_type failed.\n"); pr_err("sepol: copy default_type failed.\n");
goto exit; goto exit;
} }
char *real_object; char *real_object;
if (data.sepol5 == NULL) { if (sepol5 == NULL) {
real_object = NULL; real_object = NULL;
} else { } else {
if (strncpy_from_user(object, data.sepol5, if (strncpy_from_user(object, sepol5,
sizeof(object)) < 0) { sizeof(object)) < 0) {
pr_err("sepol: copy object failed.\n"); pr_err("sepol: copy object failed.\n");
goto exit; goto exit;
@@ -396,19 +468,19 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char cls[MAX_SEPOL_LEN]; char cls[MAX_SEPOL_LEN];
char default_type[MAX_SEPOL_LEN]; char default_type[MAX_SEPOL_LEN];
if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) {
pr_err("sepol: copy src failed.\n"); pr_err("sepol: copy src failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) { if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) {
pr_err("sepol: copy tgt failed.\n"); pr_err("sepol: copy tgt failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) { if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) {
pr_err("sepol: copy cls failed.\n"); pr_err("sepol: copy cls failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(default_type, data.sepol4, if (strncpy_from_user(default_type, sepol4,
sizeof(default_type)) < 0) { sizeof(default_type)) < 0) {
pr_err("sepol: copy default_type failed.\n"); pr_err("sepol: copy default_type failed.\n");
goto exit; goto exit;
@@ -429,15 +501,15 @@ int handle_sepolicy(unsigned long arg3, void __user *arg4)
char name[MAX_SEPOL_LEN]; char name[MAX_SEPOL_LEN];
char path[MAX_SEPOL_LEN]; char path[MAX_SEPOL_LEN];
char context[MAX_SEPOL_LEN]; char context[MAX_SEPOL_LEN];
if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) { if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) {
pr_err("sepol: copy name failed.\n"); pr_err("sepol: copy name failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) { if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) {
pr_err("sepol: copy path failed.\n"); pr_err("sepol: copy path failed.\n");
goto exit; goto exit;
} }
if (strncpy_from_user(context, data.sepol3, sizeof(context)) < if (strncpy_from_user(context, sepol3, sizeof(context)) <
0) { 0) {
pr_err("sepol: copy context failed.\n"); pr_err("sepol: copy context failed.\n");
goto exit; goto exit;

View File

@@ -41,7 +41,7 @@ setup_kernelsu() {
echo "[+] Setting up KernelSU..." echo "[+] Setting up KernelSU..."
# Clone the repository and rename it to KernelSU # Clone the repository and rename it to KernelSU
if [ ! -d "$GKI_ROOT/KernelSU" ]; then if [ ! -d "$GKI_ROOT/KernelSU" ]; then
git clone https://github.com/ShirkNeko/SukiSU-Ultra SukiSU-Ultra git clone https://github.com/SukiSU-Ultra/SukiSU-Ultra SukiSU-Ultra
mv SukiSU-Ultra KernelSU mv SukiSU-Ultra KernelSU
echo "[+] Repository cloned and renamed to KernelSU." echo "[+] Repository cloned and renamed to KernelSU."
fi fi

View File

@@ -22,6 +22,10 @@
extern void escape_to_root(); extern void escape_to_root();
#ifndef CONFIG_KPROBES
static bool ksu_sucompat_non_kp __read_mostly = true;
#endif
static void __user *userspace_stack_buffer(const void *d, size_t len) static void __user *userspace_stack_buffer(const void *d, size_t len)
{ {
/* To avoid having to mmap a page in userspace, just write below the stack /* To avoid having to mmap a page in userspace, just write below the stack
@@ -50,6 +54,12 @@ int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
{ {
const char su[] = SU_PATH; const char su[] = SU_PATH;
#ifndef CONFIG_KPROBES
if (!ksu_sucompat_non_kp) {
return 0;
}
#endif
if (!ksu_is_allow_uid(current_uid().val)) { if (!ksu_is_allow_uid(current_uid().val)) {
return 0; return 0;
} }
@@ -71,6 +81,11 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
// const char sh[] = SH_PATH; // const char sh[] = SH_PATH;
const char su[] = SU_PATH; const char su[] = SU_PATH;
#ifndef CONFIG_KPROBES
if (!ksu_sucompat_non_kp) {
return 0;
}
#endif
if (!ksu_is_allow_uid(current_uid().val)) { if (!ksu_is_allow_uid(current_uid().val)) {
return 0; return 0;
} }
@@ -115,6 +130,11 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
const char sh[] = KSUD_PATH; const char sh[] = KSUD_PATH;
const char su[] = SU_PATH; const char su[] = SU_PATH;
#ifndef CONFIG_KPROBES
if (!ksu_sucompat_non_kp) {
return 0;
}
#endif
if (unlikely(!filename_ptr)) if (unlikely(!filename_ptr))
return 0; return 0;
@@ -144,6 +164,11 @@ int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
const char su[] = SU_PATH; const char su[] = SU_PATH;
char path[sizeof(su) + 1]; char path[sizeof(su) + 1];
#ifndef CONFIG_KPROBES
if (!ksu_sucompat_non_kp){
return 0;
}
#endif
if (unlikely(!filename_user)) if (unlikely(!filename_user))
return 0; return 0;
@@ -237,6 +262,9 @@ void ksu_sucompat_init()
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre); su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre); su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre); su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
#else
ksu_sucompat_non_kp = true;
pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n");
#endif #endif
} }
@@ -246,5 +274,8 @@ void ksu_sucompat_exit()
for (int i = 0; i < ARRAY_SIZE(su_kps); i++) { for (int i = 0; i < ARRAY_SIZE(su_kps); i++) {
destroy_kprobe(&su_kps[i]); destroy_kprobe(&su_kps[i]);
} }
#else
ksu_sucompat_non_kp = false;
pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n");
#endif #endif
} }

View File

@@ -214,7 +214,8 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
int i, stop = 0; int i, stop = 0;
struct list_head data_path_list; struct list_head data_path_list;
INIT_LIST_HEAD(&data_path_list); INIT_LIST_HEAD(&data_path_list);
unsigned long data_app_magic = 0;
// Initialize APK cache list // Initialize APK cache list
struct apk_path_hash *pos, *n; struct apk_path_hash *pos, *n;
list_for_each_entry(pos, &apk_path_hash_list, list) { list_for_each_entry(pos, &apk_path_hash_list, list) {
@@ -245,6 +246,24 @@ void search_manager(const char *path, int depth, struct list_head *uid_data)
pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file)); pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file));
goto skip_iterate; goto skip_iterate;
} }
// grab magic on first folder, which is /data/app
if (!data_app_magic) {
if (file->f_inode->i_sb->s_magic) {
data_app_magic = file->f_inode->i_sb->s_magic;
pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, pos->dirpath, data_app_magic);
} else {
filp_close(file, NULL);
goto skip_iterate;
}
}
if (file->f_inode->i_sb->s_magic != data_app_magic) {
pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", __func__, pos->dirpath,
file->f_inode->i_sb->s_magic, data_app_magic);
filp_close(file, NULL);
goto skip_iterate;
}
iterate_dir(file, &ctx.ctx); iterate_dir(file, &ctx.ctx);
filp_close(file, NULL); filp_close(file, NULL);
@@ -358,12 +377,14 @@ void track_throne()
if (ksu_is_manager_uid_valid()) { if (ksu_is_manager_uid_valid()) {
pr_info("manager is uninstalled, invalidate it!\n"); pr_info("manager is uninstalled, invalidate it!\n");
ksu_invalidate_manager_uid(); ksu_invalidate_manager_uid();
goto prune;
} }
pr_info("Searching manager...\n"); pr_info("Searching manager...\n");
search_manager("/data/app", 2, &uid_list); search_manager("/data/app", 2, &uid_list);
pr_info("Search manager finished\n"); pr_info("Search manager finished\n");
} }
prune:
// then prune the allowlist // then prune the allowlist
ksu_prune_allowlist(is_uid_exist, &uid_list); ksu_prune_allowlist(is_uid_exist, &uid_list);
out: out:

View File

@@ -50,7 +50,6 @@ android {
} }
buildFeatures { buildFeatures {
aidl = true
buildConfig = true buildConfig = true
compose = true compose = true
prefab = true prefab = true
@@ -109,6 +108,7 @@ android {
} }
dependencies { dependencies {
implementation(libs.gson)
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)

View File

@@ -32,6 +32,19 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity-alias
android:name=".ui.MainActivityAlias"
android:exported="true"
android:enabled="false"
android:icon="@mipmap/ic_launcher_alt"
android:roundIcon="@mipmap/ic_launcher_alt_round"
android:targetActivity=".ui.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>
<activity <activity
android:name=".ui.webui.WebUIActivity" android:name=".ui.webui.WebUIActivity"
android:autoRemoveFromRecents="true" android:autoRemoveFromRecents="true"

View File

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

Binary file not shown.

Binary file not shown.

View File

@@ -1,4 +1,3 @@
# For more information about using CMake with Android Studio, read the # For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html # documentation: https://d.android.com/studio/projects/add-native-code.html
@@ -7,14 +6,11 @@ cmake_minimum_required(VERSION 3.18.1)
project("kernelsu") project("kernelsu")
find_package(cxx REQUIRED CONFIG)
link_libraries(cxx::cxx)
add_library(zako add_library(zako
SHARED SHARED
jni.cc jni.c
ksu.cc ksu.c
) )
find_library(log-lib log) find_library(log-lib log)

View File

@@ -0,0 +1,348 @@
#include "prelude.h"
#include "ksu.h"
#include <jni.h>
#include <sys/prctl.h>
#include <android/log.h>
#include <string.h>
NativeBridge(becomeManager, jboolean, jstring pkg) {
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, JNI_FALSE);
bool result = become_manager(cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
return result;
}
NativeBridgeNP(getVersion, jint) {
return get_version();
}
NativeBridgeNP(getAllowList, jintArray) {
int uids[1024];
int size = 0;
bool result = get_allow_list(uids, &size);
LogDebug("getAllowList: %d, size: %d", result, size);
if (result) {
jintArray array = GetEnvironment()->NewIntArray(env, size);
GetEnvironment()->SetIntArrayRegion(env, array, 0, size, uids);
return array;
}
return GetEnvironment()->NewIntArray(env, 0);
}
NativeBridgeNP(isSafeMode, jboolean) {
return is_safe_mode();
}
NativeBridgeNP(isLkmMode, jboolean) {
return is_lkm_mode();
}
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
for (int i = 0; i < count; ++i) {
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]);
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
}
}
static void addIntToList(JNIEnv *env, jobject list, int ele) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "<init>", "(I)V");
jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele);
GetEnvironment()->CallBooleanMethod(env, list, add, integer);
}
static uint64_t capListToBits(JNIEnv *env, jobject list) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
jint listSize = GetEnvironment()->CallIntMethod(env, list, size);
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
uint64_t result = 0;
for (int i = 0; i < listSize; ++i) {
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
int data = GetEnvironment()->CallIntMethod(env, integer, intValue);
if (cap_valid(data)) {
result |= (1ULL << data);
}
}
return result;
}
static int getListSize(JNIEnv *env, jobject list) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I");
return GetEnvironment()->CallIntMethod(env, list, size);
}
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
jclass cls = GetEnvironment()->GetObjectClass(env, list);
jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;");
jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer");
jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I");
for (int i = 0; i < count; ++i) {
jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i);
data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue);
}
}
NativeBridge(getAppProfile, jobject, jstring pkg, jint uid) {
if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) {
return NULL;
}
char key[KSU_MAX_PACKAGE_NAME] = { 0 };
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr);
strcpy(key, cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg);
struct app_profile profile = { 0 };
profile.version = KSU_APP_PROFILE_VER;
strcpy(profile.key, key);
profile.current_uid = uid;
bool useDefaultProfile = !get_app_profile(key, &profile);
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key));
GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid);
if (useDefaultProfile) {
// no profile found, so just use default profile:
// don't allow root and use default profile!
LogDebug("use default profile for: %s, %d", key, uid);
// allow_su = false
// non root use default = true
GetEnvironment()->SetBooleanField(env, obj, allowSuField, false);
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true);
return obj;
}
bool allowSu = profile.allow_su;
if (allowSu) {
GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
if (strlen(profile.rp_config.template_name) > 0) {
GetEnvironment()->SetObjectField(env, obj, rootTemplateField,
GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name));
}
GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid);
GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid);
jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField);
int groupCount = profile.rp_config.profile.groups_count;
if (groupCount > KSU_MAX_GROUPS) {
LogDebug("kernel group count too large: %d???", groupCount);
groupCount = KSU_MAX_GROUPS;
}
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField);
for (int i = 0; i <= CAP_LAST_CAP; i++) {
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
addIntToList(env, capList, i);
}
}
GetEnvironment()->SetObjectField(env, obj, domainField,
GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain));
GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces);
GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su);
} else {
GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default);
GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules);
}
return obj;
}
NativeBridge(setAppProfile, jboolean, jobject profile) {
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile");
jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;");
jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I");
jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z");
jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z");
jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;");
jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I");
jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I");
jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;");
jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;");
jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;");
jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I");
jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z");
jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z");
jobject key = GetEnvironment()->GetObjectField(env, profile, keyField);
if (!key) {
return false;
}
if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) {
return false;
}
const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr);
char p_key[KSU_MAX_PACKAGE_NAME] = { 0 };
strcpy(p_key, cpkg);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg);
jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField);
jint uid = GetEnvironment()->GetIntField(env, profile, uidField);
jint gid = GetEnvironment()->GetIntField(env, profile, gidField);
jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField);
jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField);
jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField);
jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField);
jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField);
struct app_profile p = { 0 };
p.version = KSU_APP_PROFILE_VER;
strcpy(p.key, p_key);
p.allow_su = allowSu;
p.current_uid = currentUid;
if (allowSu) {
p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField);
jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField);
if (templateName) {
const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr);
strcpy(p.rp_config.template_name, ctemplateName);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName);
}
p.rp_config.profile.uid = uid;
p.rp_config.profile.gid = gid;
int groups_count = getListSize(env, groups);
if (groups_count > KSU_MAX_GROUPS) {
LogDebug("groups count too large: %d", groups_count);
return false;
}
p.rp_config.profile.groups_count = groups_count;
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr);
strcpy(p.rp_config.profile.selinux_domain, cdomain);
GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain);
p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField);
} else {
p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField);
p.nrp_config.profile.umount_modules = umountModules;
}
return set_app_profile(&p);
}
NativeBridge(uidShouldUmount, jboolean, jint uid) {
return uid_should_umount(uid);
}
NativeBridgeNP(isSuEnabled, jboolean) {
return is_su_enabled();
}
NativeBridge(setSuEnabled, jboolean, jboolean enabled) {
return set_su_enabled(enabled);
}
NativeBridgeNP(isKPMEnabled, jboolean) {
return is_KPM_enable();
}
NativeBridgeNP(getHookType, jstring) {
char hook_type[16];
get_hook_type(hook_type, sizeof(hook_type));
return GetEnvironment()->NewStringUTF(env, hook_type);
}
NativeBridgeNP(getSusfsFeatureStatus, jobject) {
struct susfs_feature_status status;
bool result = get_susfs_feature_status(&status);
if (!result) {
return NULL;
}
jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$SusfsFeatureStatus");
jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "<init>", "()V");
jobject obj = GetEnvironment()->NewObject(env, cls, constructor);
// 设置各个字段
jfieldID statusSusPathField = GetEnvironment()->GetFieldID(env, cls, "statusSusPath", "Z");
jfieldID statusSusMountField = GetEnvironment()->GetFieldID(env, cls, "statusSusMount", "Z");
jfieldID statusAutoDefaultMountField = GetEnvironment()->GetFieldID(env, cls, "statusAutoDefaultMount", "Z");
jfieldID statusAutoBindMountField = GetEnvironment()->GetFieldID(env, cls, "statusAutoBindMount", "Z");
jfieldID statusSusKstatField = GetEnvironment()->GetFieldID(env, cls, "statusSusKstat", "Z");
jfieldID statusTryUmountField = GetEnvironment()->GetFieldID(env, cls, "statusTryUmount", "Z");
jfieldID statusAutoTryUmountBindField = GetEnvironment()->GetFieldID(env, cls, "statusAutoTryUmountBind", "Z");
jfieldID statusSpoofUnameField = GetEnvironment()->GetFieldID(env, cls, "statusSpoofUname", "Z");
jfieldID statusEnableLogField = GetEnvironment()->GetFieldID(env, cls, "statusEnableLog", "Z");
jfieldID statusHideSymbolsField = GetEnvironment()->GetFieldID(env, cls, "statusHideSymbols", "Z");
jfieldID statusSpoofCmdlineField = GetEnvironment()->GetFieldID(env, cls, "statusSpoofCmdline", "Z");
jfieldID statusOpenRedirectField = GetEnvironment()->GetFieldID(env, cls, "statusOpenRedirect", "Z");
jfieldID statusMagicMountField = GetEnvironment()->GetFieldID(env, cls, "statusMagicMount", "Z");
jfieldID statusSusSuField = GetEnvironment()->GetFieldID(env, cls, "statusSusSu", "Z");
GetEnvironment()->SetBooleanField(env, obj, statusSusPathField, status.status_sus_path);
GetEnvironment()->SetBooleanField(env, obj, statusSusMountField, status.status_sus_mount);
GetEnvironment()->SetBooleanField(env, obj, statusAutoDefaultMountField, status.status_auto_default_mount);
GetEnvironment()->SetBooleanField(env, obj, statusAutoBindMountField, status.status_auto_bind_mount);
GetEnvironment()->SetBooleanField(env, obj, statusSusKstatField, status.status_sus_kstat);
GetEnvironment()->SetBooleanField(env, obj, statusTryUmountField, status.status_try_umount);
GetEnvironment()->SetBooleanField(env, obj, statusAutoTryUmountBindField, status.status_auto_try_umount_bind);
GetEnvironment()->SetBooleanField(env, obj, statusSpoofUnameField, status.status_spoof_uname);
GetEnvironment()->SetBooleanField(env, obj, statusEnableLogField, status.status_enable_log);
GetEnvironment()->SetBooleanField(env, obj, statusHideSymbolsField, status.status_hide_symbols);
GetEnvironment()->SetBooleanField(env, obj, statusSpoofCmdlineField, status.status_spoof_cmdline);
GetEnvironment()->SetBooleanField(env, obj, statusOpenRedirectField, status.status_open_redirect);
GetEnvironment()->SetBooleanField(env, obj, statusMagicMountField, status.status_magic_mount);
GetEnvironment()->SetBooleanField(env, obj, statusSusSuField, status.status_sus_su);
return obj;
}

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_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) {
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
auto result = become_manager(cpkg);
env->ReleaseStringUTFChars(pkg, cpkg);
return result;
}
extern "C"
JNIEXPORT jint JNICALL
Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) {
return get_version();
}
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) {
int uids[1024];
int size = 0;
bool result = get_allow_list(uids, &size);
LOGD("getAllowList: %d, size: %d", result, size);
if (result) {
auto array = env->NewIntArray(size);
env->SetIntArrayRegion(array, 0, size, uids);
return array;
}
return env->NewIntArray(0);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) {
return is_safe_mode();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_isLkmMode(JNIEnv *env, jclass clazz) {
return is_lkm_mode();
}
static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {
auto cls = env->GetObjectClass(list);
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
auto integerCls = env->FindClass("java/lang/Integer");
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
for (int i = 0; i < count; ++i) {
auto integer = env->NewObject(integerCls, constructor, data[i]);
env->CallBooleanMethod(list, add, integer);
}
}
static void addIntToList(JNIEnv *env, jobject list, int ele) {
auto cls = env->GetObjectClass(list);
auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z");
auto integerCls = env->FindClass("java/lang/Integer");
auto constructor = env->GetMethodID(integerCls, "<init>", "(I)V");
auto integer = env->NewObject(integerCls, constructor, ele);
env->CallBooleanMethod(list, add, integer);
}
static uint64_t capListToBits(JNIEnv *env, jobject list) {
auto cls = env->GetObjectClass(list);
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
auto size = env->GetMethodID(cls, "size", "()I");
auto listSize = env->CallIntMethod(list, size);
auto integerCls = env->FindClass("java/lang/Integer");
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
uint64_t result = 0;
for (int i = 0; i < listSize; ++i) {
auto integer = env->CallObjectMethod(list, get, i);
int data = env->CallIntMethod(integer, intValue);
if (cap_valid(data)) {
result |= (1ULL << data);
}
}
return result;
}
static int getListSize(JNIEnv *env, jobject list) {
auto cls = env->GetObjectClass(list);
auto size = env->GetMethodID(cls, "size", "()I");
return env->CallIntMethod(list, size);
}
static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {
auto cls = env->GetObjectClass(list);
auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;");
auto integerCls = env->FindClass("java/lang/Integer");
auto intValue = env->GetMethodID(integerCls, "intValue", "()I");
for (int i = 0; i < count; ++i) {
auto integer = env->CallObjectMethod(list, get, i);
data[i] = env->CallIntMethod(integer, intValue);
}
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {
if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {
return nullptr;
}
p_key_t key = {};
auto cpkg = env->GetStringUTFChars(pkg, nullptr);
strcpy(key, cpkg);
env->ReleaseStringUTFChars(pkg, cpkg);
app_profile profile = {};
profile.version = KSU_APP_PROFILE_VER;
strcpy(profile.key, key);
profile.current_uid = uid;
bool useDefaultProfile = !get_app_profile(key, &profile);
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
auto constructor = env->GetMethodID(cls, "<init>", "()V");
auto obj = env->NewObject(cls, constructor);
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
auto uidField = env->GetFieldID(cls, "uid", "I");
auto gidField = env->GetFieldID(cls, "gid", "I");
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key));
env->SetIntField(obj, currentUidField, profile.current_uid);
if (useDefaultProfile) {
// no profile found, so just use default profile:
// don't allow root and use default profile!
LOGD("use default profile for: %s, %d", key, uid);
// allow_su = false
// non root use default = true
env->SetBooleanField(obj, allowSuField, false);
env->SetBooleanField(obj, nonRootUseDefaultField, true);
return obj;
}
auto allowSu = profile.allow_su;
if (allowSu) {
env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);
if (strlen(profile.rp_config.template_name) > 0) {
env->SetObjectField(obj, rootTemplateField,
env->NewStringUTF(profile.rp_config.template_name));
}
env->SetIntField(obj, uidField, profile.rp_config.profile.uid);
env->SetIntField(obj, gidField, profile.rp_config.profile.gid);
jobject groupList = env->GetObjectField(obj, groupsField);
int groupCount = profile.rp_config.profile.groups_count;
if (groupCount > KSU_MAX_GROUPS) {
LOGD("kernel group count too large: %d???", groupCount);
groupCount = KSU_MAX_GROUPS;
}
fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);
jobject capList = env->GetObjectField(obj, capabilitiesField);
for (int i = 0; i <= CAP_LAST_CAP; i++) {
if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {
addIntToList(env, capList, i);
}
}
env->SetObjectField(obj, domainField,
env->NewStringUTF(profile.rp_config.profile.selinux_domain));
env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces);
env->SetBooleanField(obj, allowSuField, profile.allow_su);
} else {
env->SetBooleanField(obj, nonRootUseDefaultField,
(jboolean) profile.nrp_config.use_default);
env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules);
}
return obj;
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {
auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile");
auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;");
auto currentUidField = env->GetFieldID(cls, "currentUid", "I");
auto allowSuField = env->GetFieldID(cls, "allowSu", "Z");
auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z");
auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;");
auto uidField = env->GetFieldID(cls, "uid", "I");
auto gidField = env->GetFieldID(cls, "gid", "I");
auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;");
auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;");
auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;");
auto namespacesField = env->GetFieldID(cls, "namespace", "I");
auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z");
auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z");
auto key = env->GetObjectField(profile, keyField);
if (!key) {
return false;
}
if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) {
return false;
}
auto cpkg = env->GetStringUTFChars((jstring) key, nullptr);
p_key_t p_key = {};
strcpy(p_key, cpkg);
env->ReleaseStringUTFChars((jstring) key, cpkg);
auto currentUid = env->GetIntField(profile, currentUidField);
auto uid = env->GetIntField(profile, uidField);
auto gid = env->GetIntField(profile, gidField);
auto groups = env->GetObjectField(profile, groupsField);
auto capabilities = env->GetObjectField(profile, capabilitiesField);
auto domain = env->GetObjectField(profile, domainField);
auto allowSu = env->GetBooleanField(profile, allowSuField);
auto umountModules = env->GetBooleanField(profile, umountModulesField);
app_profile p = {};
p.version = KSU_APP_PROFILE_VER;
strcpy(p.key, p_key);
p.allow_su = allowSu;
p.current_uid = currentUid;
if (allowSu) {
p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField);
auto templateName = env->GetObjectField(profile, rootTemplateField);
if (templateName) {
auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr);
strcpy(p.rp_config.template_name, ctemplateName);
env->ReleaseStringUTFChars((jstring) templateName, ctemplateName);
}
p.rp_config.profile.uid = uid;
p.rp_config.profile.gid = gid;
int groups_count = getListSize(env, groups);
if (groups_count > KSU_MAX_GROUPS) {
LOGD("groups count too large: %d", groups_count);
return false;
}
p.rp_config.profile.groups_count = groups_count;
fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);
p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);
auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr);
strcpy(p.rp_config.profile.selinux_domain, cdomain);
env->ReleaseStringUTFChars((jstring) domain, cdomain);
p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField);
} else {
p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField);
p.nrp_config.profile.umount_modules = umountModules;
}
return set_app_profile(&p);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {
return uid_should_umount(uid);
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {
return is_su_enabled();
}
extern "C"
JNIEXPORT jboolean JNICALL
Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {
return set_su_enabled(enabled);
}

View File

@@ -8,6 +8,7 @@
#include <stdio.h> #include <stdio.h>
#include <unistd.h> #include <unistd.h>
#include "prelude.h"
#include "ksu.h" #include "ksu.h"
#define KERNEL_SU_OPTION 0xDEADBEEF #define KERNEL_SU_OPTION 0xDEADBEEF
@@ -29,11 +30,15 @@
#define CMD_IS_UID_SHOULD_UMOUNT 13 #define CMD_IS_UID_SHOULD_UMOUNT 13
#define CMD_IS_SU_ENABLED 14 #define CMD_IS_SU_ENABLED 14
#define CMD_ENABLE_SU 15 #define CMD_ENABLE_SU 15
#define CMD_ENABLE_KPM 100
#define CMD_HOOK_TYPE 101
#define CMD_GET_SUSFS_FEATURE_STATUS 102
static bool ksuctl(int cmd, void* arg1, void* arg2) { static bool ksuctl(int cmd, void* arg1, void* arg2) {
int32_t result = 0; int32_t result = 0;
prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result); int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result);
return result == KERNEL_SU_OPTION;
return result == KERNEL_SU_OPTION && rtn == -1;
} }
bool become_manager(const char* pkg) { bool become_manager(const char* pkg) {
@@ -46,7 +51,7 @@ bool become_manager(const char* pkg) {
snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg); snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg);
} }
return ksuctl(CMD_BECOME_MANAGER, param, nullptr); return ksuctl(CMD_BECOME_MANAGER, param, NULL);
} }
// cache the result to avoid unnecessary syscall // cache the result to avoid unnecessary syscall
@@ -66,7 +71,7 @@ bool get_allow_list(int *uids, int *size) {
} }
bool is_safe_mode() { bool is_safe_mode() {
return ksuctl(CMD_CHECK_SAFEMODE, nullptr, nullptr); return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL);
} }
bool is_lkm_mode() { bool is_lkm_mode() {
@@ -75,25 +80,56 @@ bool is_lkm_mode() {
} }
bool uid_should_umount(int uid) { bool uid_should_umount(int uid) {
bool should; int should;
return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, reinterpret_cast<void*>(uid), &should) && should; return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should;
} }
bool set_app_profile(const app_profile *profile) { bool set_app_profile(const struct app_profile* profile) {
return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, nullptr); return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL);
} }
bool get_app_profile(p_key_t key, app_profile *profile) { bool get_app_profile(char* key, struct app_profile* profile) {
return ksuctl(CMD_GET_APP_PROFILE, (void*) profile, nullptr); return ksuctl(CMD_GET_APP_PROFILE, profile, NULL);
} }
bool set_su_enabled(bool enabled) { bool set_su_enabled(bool enabled) {
return ksuctl(CMD_ENABLE_SU, (void*) enabled, nullptr); return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL);
} }
bool is_su_enabled() { bool is_su_enabled() {
bool enabled = true; int enabled = true;
// if ksuctl failed, we assume su is enabled, and it cannot be disabled. // if ksuctl failed, we assume su is enabled, and it cannot be disabled.
ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr); ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL);
return enabled; 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);
} }

View File

@@ -5,6 +5,7 @@
#ifndef KERNELSU_KSU_H #ifndef KERNELSU_KSU_H
#define KERNELSU_KSU_H #define KERNELSU_KSU_H
#include "prelude.h"
#include <linux/capability.h> #include <linux/capability.h>
bool become_manager(const char *); bool become_manager(const char *);
@@ -25,7 +26,24 @@ bool is_lkm_mode();
#define KSU_MAX_GROUPS 32 #define KSU_MAX_GROUPS 32
#define KSU_SELINUX_DOMAIN 64 #define KSU_SELINUX_DOMAIN 64
using p_key_t = char[KSU_MAX_PACKAGE_NAME]; // SUSFS Functional State Structures
struct susfs_feature_status {
bool status_sus_path;
bool status_sus_mount;
bool status_auto_default_mount;
bool status_auto_bind_mount;
bool status_sus_kstat;
bool status_try_umount;
bool status_auto_try_umount_bind;
bool status_spoof_uname;
bool status_enable_log;
bool status_hide_symbols;
bool status_spoof_cmdline;
bool status_open_redirect;
bool status_magic_mount;
bool status_overlayfs_auto_kstat;
bool status_sus_su;
};
struct root_profile { struct root_profile {
int32_t uid; int32_t uid;
@@ -75,12 +93,18 @@ struct app_profile {
}; };
}; };
bool set_app_profile(const app_profile *profile); bool set_app_profile(const struct app_profile* profile);
bool get_app_profile(p_key_t key, app_profile *profile); bool get_app_profile(char* key, struct app_profile* profile);
bool set_su_enabled(bool enabled); bool set_su_enabled(bool enabled);
bool is_su_enabled(); bool is_su_enabled();
#endif //KERNELSU_KSU_H bool is_KPM_enable();
bool get_hook_type(char* hook_type, size_t size);
bool get_susfs_feature_status(struct susfs_feature_status* status);
#endif //KERNELSU_KSU_H

View File

@@ -0,0 +1,17 @@
#ifndef KERNELSU_PRELUDE_H
#define KERNELSU_PRELUDE_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <jni.h>
#include <android/log.h>
#define GetEnvironment() (*env)
#define NativeBridge(fn, rtn, ...) JNIEXPORT rtn JNICALL Java_com_sukisu_ultra_Natives_##fn(JNIEnv* env, jclass clazz, __VA_ARGS__)
#define NativeBridgeNP(fn, rtn) JNIEXPORT rtn JNICALL Java_com_sukisu_ultra_Natives_##fn(JNIEnv* env, jclass clazz)
#define LogDebug(...) __android_log_print(ANDROID_LOG_DEBUG, "KernelSU", __VA_ARGS__)
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -1,21 +1,95 @@
package com.sukisu.ultra package com.sukisu.ultra
import android.annotation.SuppressLint
import android.app.Activity
import android.app.ActivityOptions
import android.app.Application import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Bundle
import coil.Coil import coil.Coil
import coil.ImageLoader import coil.ImageLoader
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconFetcher
import me.zhanghai.android.appiconloader.coil.AppIconKeyer import me.zhanghai.android.appiconloader.coil.AppIconKeyer
import java.io.File import java.io.File
import java.util.Locale
@SuppressLint("StaticFieldLeak")
lateinit var ksuApp: KernelSUApplication lateinit var ksuApp: KernelSUApplication
class KernelSUApplication : Application() { class KernelSUApplication : Application() {
private var currentActivity: Activity? = null
private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
currentActivity = activity
}
override fun onActivityStarted(activity: Activity) {
currentActivity = activity
}
override fun onActivityResumed(activity: Activity) {
currentActivity = activity
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {
if (currentActivity == activity) {
currentActivity = null
}
}
}
override fun attachBaseContext(base: Context) {
val prefs = base.getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var context = base
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(base.resources.configuration)
config.setLocale(locale)
context = base.createConfigurationContext(config)
}
super.attachBaseContext(context)
}
@SuppressLint("ObsoleteSdkInt")
override fun getResources(): Resources {
val resources = super.getResources()
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return createConfigurationContext(config).resources
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
return resources
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
ksuApp = this ksuApp = this
// 注册Activity生命周期回调
registerActivityLifecycleCallbacks(activityLifecycleCallbacks)
Platform.setHiddenApiExemptions() Platform.setHiddenApiExemptions()
val context = this val context = this
@@ -35,5 +109,43 @@ class KernelSUApplication : Application() {
} }
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
applyLanguageSetting()
}
} @SuppressLint("ObsoleteSdkInt")
private fun applyLanguageSetting() {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
// 添加刷新当前Activity的方法
fun refreshCurrentActivity() {
currentActivity?.let { activity ->
val intent = activity.intent
activity.finish()
val options = ActivityOptions.makeCustomAnimation(
activity, android.R.anim.fade_in, android.R.anim.fade_out
)
activity.startActivity(intent, options.toBundle())
}
}
}

View File

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

View File

@@ -16,16 +16,18 @@ object Natives {
// 10946: add capabilities // 10946: add capabilities
// 10977: change groups_count and groups to avoid overflow write // 10977: change groups_count and groups to avoid overflow write
// 11071: Fix the issue of failing to set a custom SELinux type. // 11071: Fix the issue of failing to set a custom SELinux type.
const val MINIMAL_SUPPORTED_KERNEL = 12800 const val MINIMAL_SUPPORTED_KERNEL = 11071
// 11640: Support query working mode, LKM or GKI // 11640: Support query working mode, LKM or GKI
// when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant. // when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant.
const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800 const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648
// 12040: Support disable sucompat mode // 12040: Support disable sucompat mode
const val MINIMAL_SUPPORTED_SU_COMPAT = 12800 const val MINIMAL_SUPPORTED_SU_COMPAT = 12040
const val KERNEL_SU_DOMAIN = "u:r:su:s0" const val KERNEL_SU_DOMAIN = "u:r:su:s0"
const val MINIMAL_SUPPORTED_KPM = 12800
const val ROOT_UID = 0 const val ROOT_UID = 0
const val ROOT_GID = 0 const val ROOT_GID = 0
@@ -66,6 +68,14 @@ object Natives {
*/ */
external fun isSuEnabled(): Boolean external fun isSuEnabled(): Boolean
external fun setSuEnabled(enabled: Boolean): Boolean external fun setSuEnabled(enabled: Boolean): Boolean
external fun isKPMEnabled(): Boolean
external fun getHookType(): String
/**
* Get SUSFS feature status from kernel
* @return SusfsFeatureStatus object containing all feature states, or null if failed
*/
external fun getSusfsFeatureStatus(): SusfsFeatureStatus?
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
private const val NOBODY_UID = 9999 private const val NOBODY_UID = 9999
@@ -91,6 +101,48 @@ object Natives {
return version < MINIMAL_SUPPORTED_KERNEL return version < MINIMAL_SUPPORTED_KERNEL
} }
@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 {
fun toMap(): Map<String, Boolean> {
return mapOf(
"CONFIG_KSU_SUSFS_SUS_PATH" to statusSusPath,
"CONFIG_KSU_SUSFS_SUS_MOUNT" to statusSusMount,
"CONFIG_KSU_SUSFS_AUTO_ADD_SUS_KSU_DEFAULT_MOUNT" to statusAutoDefaultMount,
"CONFIG_KSU_SUSFS_AUTO_ADD_SUS_BIND_MOUNT" to statusAutoBindMount,
"CONFIG_KSU_SUSFS_SUS_KSTAT" to statusSusKstat,
"CONFIG_KSU_SUSFS_TRY_UMOUNT" to statusTryUmount,
"CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to statusAutoTryUmountBind,
"CONFIG_KSU_SUSFS_SPOOF_UNAME" to statusSpoofUname,
"CONFIG_KSU_SUSFS_ENABLE_LOG" to statusEnableLog,
"CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to statusHideSymbols,
"CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to statusSpoofCmdline,
"CONFIG_KSU_SUSFS_OPEN_REDIRECT" to statusOpenRedirect,
"CONFIG_KSU_SUSFS_HAS_MAGIC_MOUNT" to statusMagicMount,
"CONFIG_KSU_SUSFS_SUS_OVERLAYFS" to statusOverlayfsAutoKstat,
"CONFIG_KSU_SUSFS_SUS_SU" to statusSusSu
)
}
}
@Immutable @Immutable
@Parcelize @Parcelize
@Keep @Keep

View File

@@ -1,157 +1,183 @@
package com.sukisu.ultra.ui package com.sukisu.ultra.ui
import android.content.Context import android.content.Context
import android.database.ContentObserver import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.*
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.lifecycleScope
import androidx.compose.ui.res.stringResource import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.ramcosta.composedestinations.DestinationsNavHost import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination
import com.ramcosta.composedestinations.spec.NavHostGraphSpec import com.ramcosta.composedestinations.spec.NavHostGraphSpec
import com.ramcosta.composedestinations.spec.RouteOrDirection
import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import io.sukisu.ultra.UltraToolInstall import io.sukisu.ultra.UltraToolInstall
import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.screen.BottomBarDestination import zako.zako.zako.zakoui.activity.util.AppData
import com.sukisu.ultra.ui.theme.* import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import zako.zako.zako.zakoui.activity.util.*
import com.sukisu.ultra.ui.util.* import zako.zako.zako.zakoui.activity.component.BottomBar
import androidx.core.content.edit import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.util.install
import com.sukisu.ultra.ui.viewmodel.HomeViewModel
import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel
import com.sukisu.ultra.ui.webui.initPlatform import com.sukisu.ultra.ui.webui.initPlatform
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private inner class ThemeChangeContentObserver( private lateinit var superUserViewModel: SuperUserViewModel
handler: Handler, private lateinit var homeViewModel: HomeViewModel
private val onThemeChanged: () -> Unit internal val settingsStateFlow = MutableStateFlow(SettingsState())
) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) { data class SettingsState(
super.onChange(selfChange) val isHideOtherInfo: Boolean = false,
onThemeChanged() val showKpmInfo: Boolean = false
} )
private lateinit var themeChangeObserver: ThemeChangeContentObserver
// 添加标记避免重复初始化
private var isInitialized = false
override fun attachBaseContext(newBase: Context) {
val context = LocaleUtils.applyLocale(newBase)
super.attachBaseContext(context)
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// 应用DPI设置仅对当前应用生效 try {
applyCustomDpi() // 确保应用正确的语言设置
LocaleUtils.applyLanguageSetting(this)
// Enable edge to edge // 应用自定义 DPI
enableEdgeToEdge() DisplayUtils.applyCustomDpi(this)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Enable edge to edge
window.isNavigationBarContrastEnforced = false enableEdgeToEdge()
}
super.onCreate(savedInstanceState) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val isFirstRun = prefs.getBoolean("is_first_run", true)
if (isFirstRun) {
ThemeConfig.preventBackgroundRefresh = false
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
} }
prefs.edit { putBoolean("is_first_run", false) }
}
// 加载保存的背景设置 super.onCreate(savedInstanceState)
loadThemeMode()
loadThemeColors()
loadDynamicColorState()
CardConfig.load(applicationContext)
val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) { // 使用标记控制初始化流程
runOnUiThread { if (!isInitialized) {
if (!ThemeConfig.preventBackgroundRefresh) { initializeViewModels()
ThemeConfig.backgroundImageLoaded = false initializeData()
loadCustomBackground() isInitialized = true
}
setContent {
KernelSUTheme {
val navController = rememberNavController()
val snackBarHostState = remember { SnackbarHostState() }
val currentDestination = navController.currentBackStackEntryAsState().value?.destination
val showBottomBar = when (currentDestination?.route) {
ExecuteModuleActionScreenDestination.route -> false
else -> true
}
LaunchedEffect(Unit) {
initPlatform()
}
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState
) {
Scaffold(
bottomBar = {
AnimatedBottomBar.AnimatedBottomBarWrapper(
showBottomBar = showBottomBar,
content = { BottomBar(navController) }
)
},
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root as NavHostGraphSpec,
navController = navController,
defaultTransitions = NavigationUtils.defaultTransitions()
)
}
}
} }
} }
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun initializeViewModels() {
superUserViewModel = SuperUserViewModel()
homeViewModel = HomeViewModel()
// 设置主题变化监听器
themeChangeObserver = ThemeUtils.registerThemeChangeObserver(this)
}
private fun initializeData() {
lifecycleScope.launch {
try {
superUserViewModel.fetchAppList()
} catch (e: Exception) {
e.printStackTrace()
}
} }
contentResolver.registerContentObserver( lifecycleScope.launch {
android.provider.Settings.System.getUriFor("ui_night_mode"), try {
false, homeViewModel.initializeData()
contentObserver } catch (e: Exception) {
) e.printStackTrace()
}
val destroyListeners = mutableListOf<() -> Unit>()
destroyListeners.add {
contentResolver.unregisterContentObserver(contentObserver)
} }
val isManager = Natives.becomeManager(ksuApp.packageName) // 数据刷新协程
DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope)
DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow)
// 初始化主题相关设置
ThemeUtils.initializeThemeSettings(this, settingsStateFlow)
val isManager = AppData.isManager(ksuApp.packageName)
if (isManager) { if (isManager) {
install() install()
UltraToolInstall.tryToInstall() UltraToolInstall.tryToInstall()
} }
}
setContent { override fun onResume() {
KernelSUTheme { try {
val navController = rememberNavController() super.onResume()
val snackBarHostState = remember { SnackbarHostState() } LocaleUtils.applyLanguageSetting(this)
ThemeUtils.onActivityResume()
// pre-init platform to faster start WebUI X activities // 仅在需要时刷新数据
LaunchedEffect(Unit) { if (isInitialized) {
initPlatform() refreshData()
}
Scaffold(
bottomBar = { BottomBar(navController) },
contentWindowInsets = WindowInsets(0, 0, 0, 0)
) { innerPadding ->
CompositionLocalProvider(
LocalSnackbarHost provides snackBarHostState
) {
DestinationsNavHost(
modifier = Modifier.padding(innerPadding),
navGraph = NavGraphs.root as NavHostGraphSpec,
navController = navController,
defaultTransitions = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
get() = { fadeIn(animationSpec = tween(340)) }
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
get() = { fadeOut(animationSpec = tween(340)) }
}
)
}
}
} }
} catch (e: Exception) {
e.printStackTrace()
} }
} }
// 应用自定义DPI设置仅对当前应用生效 private fun refreshData() {
private fun applyCustomDpi() { lifecycleScope.launch {
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try { try {
val resources = resources superUserViewModel.fetchAppList()
val metrics = resources.displayMetrics homeViewModel.initializeData()
DataRefreshUtils.refreshData(lifecycleScope)
// 仅更新应用内显示,不影响系统状态栏
metrics.density = customDpi / 160f
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@@ -159,125 +185,29 @@ class MainActivity : ComponentActivity() {
} }
override fun onPause() { override fun onPause() {
super.onPause() try {
CardConfig.save(applicationContext) super.onPause()
getSharedPreferences("theme_prefs", MODE_PRIVATE).edit { ThemeUtils.onActivityPause(this)
putBoolean("prevent_background_refresh", true) } catch (e: Exception) {
} e.printStackTrace()
ThemeConfig.preventBackgroundRefresh = true
}
override fun onResume() {
super.onResume()
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
loadCustomBackground()
} }
} }
private val destroyListeners = mutableListOf<() -> Unit>()
override fun onDestroy() { override fun onDestroy() {
destroyListeners.forEach { it() } try {
super.onDestroy() ThemeUtils.unregisterThemeChangeObserver(this, themeChangeObserver)
} super.onDestroy()
} } catch (e: Exception) {
e.printStackTrace()
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BottomBar(navController: NavHostController) {
val navigator = navController.rememberDestinationsNavigator()
val isManager = Natives.becomeManager(ksuApp.packageName)
val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()
val kpmVersion = getKpmVersion()
val containerColor = MaterialTheme.colorScheme.surfaceVariant
val cardColor = MaterialTheme.colorScheme.surfaceVariant
// 检查是否显示KPM
val showKpmInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("show_kpm_info", true)
NavigationBar(
modifier = Modifier.windowInsetsPadding(
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
),
containerColor = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = containerColor.copy(alpha = cardAlpha)
).containerColor,
tonalElevation = cardElevation
) {
BottomBarDestination.entries.forEach { destination ->
if (destination == BottomBarDestination.Kpm) {
if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error") && showKpmInfo) {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (!isCurrentDestOnBackStack) {
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
icon = {
Icon(
imageVector = if (isCurrentDestOnBackStack) {
destination.iconSelected
} else {
destination.iconNotSelected
},
contentDescription = stringResource(destination.label),
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
},
label = {
Text(
text = stringResource(destination.label),
style = MaterialTheme.typography.labelMedium
)
}
)
}
} else {
if (!fullFeatured && destination.rootRequired) return@forEach
val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction)
NavigationBarItem(
selected = isCurrentDestOnBackStack,
onClick = {
if (!isCurrentDestOnBackStack) {
navigator.navigate(destination.direction) {
popUpTo(NavGraphs.root as RouteOrDirection) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
},
icon = {
Icon(
imageVector = if (isCurrentDestOnBackStack) {
destination.iconSelected
} else {
destination.iconNotSelected
},
contentDescription = stringResource(destination.label),
tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
)
},
label = {
Text(
text = stringResource(destination.label),
style = MaterialTheme.typography.labelMedium
)
}
)
}
} }
} }
}
override fun onConfigurationChanged(newConfig: Configuration) {
try {
super.onConfigurationChanged(newConfig)
LocaleUtils.applyLanguageSetting(this)
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,73 @@
package com.sukisu.ultra.ui.component
import android.annotation.SuppressLint
import androidx.compose.animation.core.*
import androidx.compose.animation.*
import androidx.compose.runtime.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.dp
@SuppressLint("AutoboxingStateCreation")
@Composable
fun rememberFabVisibilityState(listState: LazyListState): State<Boolean> {
var previousScrollOffset by remember { mutableStateOf(0) }
var previousIndex by remember { mutableStateOf(0) }
val fabVisible = remember { mutableStateOf(true) }
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
.collect { (index, offset) ->
if (previousIndex == 0 && previousScrollOffset == 0) {
fabVisible.value = true
} else {
val isScrollingDown = when {
index > previousIndex -> false
index < previousIndex -> true
else -> offset < previousScrollOffset
}
fabVisible.value = isScrollingDown
}
previousIndex = index
previousScrollOffset = offset
}
}
return fabVisible
}
@Composable
fun AnimatedFab(
visible: Boolean,
content: @Composable () -> Unit
) {
val scale by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
AnimatedVisibility(
visible = visible,
enter = fadeIn() + scaleIn(),
exit = fadeOut() + scaleOut(targetScale = 0.8f)
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.scale(scale)
.alpha(scale)
) {
content()
}
}
}

View File

@@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
@@ -138,7 +137,7 @@ fun ImageEditorDialog(
0f 0f
} }
updateTransformation(newScale, newOffsetX, newOffsetY) updateTransformation(newScale, newOffsetX, newOffsetY)
} catch (e: Exception) { } catch (_: Exception) {
updateTransformation(lastScale, lastOffsetX, lastOffsetY) updateTransformation(lastScale, lastOffsetX, lastOffsetY)
} }
} }
@@ -186,7 +185,7 @@ fun ImageEditorDialog(
val transformation = BackgroundTransformation(scale, offsetX, offsetY) val transformation = BackgroundTransformation(scale, offsetX, offsetY)
val savedUri = context.saveTransformedBackground(imageUri, transformation) val savedUri = context.saveTransformedBackground(imageUri, transformation)
savedUri?.let { onConfirm(it) } savedUri?.let { onConfirm(it) }
} catch (e: Exception) { } catch (_: Exception) {
"" ""
} }
} }

View File

@@ -63,7 +63,12 @@ fun SearchAppBar(
var onSearch by remember { mutableStateOf(false) } var onSearch by remember { mutableStateOf(false) }
// 获取卡片颜色和透明度 // 获取卡片颜色和透明度
val cardColor = MaterialTheme.colorScheme.surfaceVariant val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = CardConfig.cardAlpha val cardAlpha = CardConfig.cardAlpha
if (onSearch) { if (onSearch) {

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.selection.toggleable
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -12,10 +13,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.LabelItem
import com.dergoogler.mmrl.ui.component.text.TextRow import com.dergoogler.mmrl.ui.component.text.TextRow
import com.sukisu.ultra.ui.theme.CardConfig
@Composable @Composable
fun SwitchItem( fun SwitchItem(
@@ -25,64 +28,70 @@ fun SwitchItem(
checked: Boolean, checked: Boolean,
enabled: Boolean = true, enabled: Boolean = true,
beta: Boolean = false, beta: Boolean = false,
onCheckedChange: (Boolean) -> Unit, onCheckedChange: (Boolean) -> Unit
) { ) {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) } val stateAlpha = remember(checked, enabled) { Modifier.alpha(if (enabled) 1f else 0.5f) }
ListItem( MaterialTheme(
modifier = Modifier colorScheme = MaterialTheme.colorScheme.copy(
.toggleable( surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceContainerHigh
value = checked, )
interactionSource = interactionSource, ) {
role = Role.Switch, ListItem(
enabled = enabled, modifier = Modifier
indication = LocalIndication.current, .toggleable(
onValueChange = onCheckedChange value = checked,
), interactionSource = interactionSource,
headlineContent = { role = Role.Switch,
TextRow( enabled = enabled,
leadingContent = if (beta) { indication = LocalIndication.current,
{ onValueChange = onCheckedChange
LabelItem( ),
modifier = Modifier.then(stateAlpha), headlineContent = {
text = "Beta" TextRow(
) leadingContent = if (beta) {
} {
} else null LabelItem(
) { modifier = Modifier.then(stateAlpha),
Text( text = "Beta"
modifier = Modifier.then(stateAlpha), )
text = title, }
} 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
)
}
} }
}, )
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 @Composable

View File

@@ -1,6 +1,5 @@
package com.sukisu.ultra.ui.component package com.sukisu.ultra.ui.component
import android.content.Context
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
@@ -10,12 +9,10 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.theme.ThemeConfig
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.SdStorage import androidx.compose.material.icons.filled.SdStorage
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -32,13 +29,19 @@ fun SlotSelectionDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onSlotSelected: (String) -> Unit onSlotSelected: (String) -> Unit
) { ) {
val context = LocalContext.current
var currentSlot by remember { mutableStateOf<String?>(null) } var currentSlot by remember { mutableStateOf<String?>(null) }
var errorMessage by remember { mutableStateOf<String?>(null) } var errorMessage by remember { mutableStateOf<String?>(null) }
var selectedSlot by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
try { try {
currentSlot = getCurrentSlot(context) currentSlot = getCurrentSlot()
// 设置默认选择为当前槽位
selectedSlot = when (currentSlot) {
"a" -> "a"
"b" -> "b"
else -> null
}
errorMessage = null errorMessage = null
} catch (e: Exception) { } catch (e: Exception) {
errorMessage = e.message errorMessage = e.message
@@ -103,12 +106,12 @@ fun SlotSelectionDialog(
val slotOptions = listOf( val slotOptions = listOf(
ListOption( ListOption(
titleText = stringResource(id = R.string.slot_a), titleText = stringResource(id = R.string.slot_a),
subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null, subtitleText = null,
icon = Icons.Filled.SdStorage icon = Icons.Filled.SdStorage
), ),
ListOption( ListOption(
titleText = stringResource(id = R.string.slot_b), titleText = stringResource(id = R.string.slot_b),
subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null, subtitleText = null,
icon = Icons.Filled.SdStorage icon = Icons.Filled.SdStorage
) )
) )
@@ -124,19 +127,20 @@ fun SlotSelectionDialog(
.fillMaxWidth() .fillMaxWidth()
.clip(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium)
.background( .background(
color = if (option.subtitleText != null) { color = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
} else { } else {
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
} }
) )
.clickable { .clickable {
onSlotSelected( selectedSlot = when(index) {
when (index) { 0 -> "a"
0 -> "a" else -> "b"
else -> "b" }
}
)
} }
.padding(vertical = 12.dp, horizontal = 16.dp), .padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
@@ -144,7 +148,10 @@ fun SlotSelectionDialog(
Icon( Icon(
imageVector = option.icon, imageVector = option.icon,
contentDescription = null, contentDescription = null,
tint = if (option.subtitleText != null) { tint = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.onPrimary MaterialTheme.colorScheme.onPrimary
} else { } else {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
@@ -159,7 +166,10 @@ fun SlotSelectionDialog(
Text( Text(
text = option.titleText, text = option.titleText,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = if (option.subtitleText != null) { color = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.onPrimary MaterialTheme.colorScheme.onPrimary
} else { } else {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
@@ -169,7 +179,10 @@ fun SlotSelectionDialog(
Text( Text(
text = it, text = it,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = if (true) { color = if (selectedSlot == when(index) {
0 -> "a"
else -> "b"
}) {
MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f) MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f)
} else { } else {
MaterialTheme.colorScheme.onSurfaceVariant MaterialTheme.colorScheme.onSurfaceVariant
@@ -186,9 +199,10 @@ fun SlotSelectionDialog(
confirmButton = { confirmButton = {
TextButton( TextButton(
onClick = { onClick = {
currentSlot?.let { onSlotSelected(it) } selectedSlot?.let { onSlotSelected(it) }
onDismiss() onDismiss()
} },
enabled = selectedSlot != null
) { ) {
Text( Text(
text = stringResource(android.R.string.ok), text = stringResource(android.R.string.ok),
@@ -221,7 +235,7 @@ data class ListOption(
) )
// Utility function to get current slot // Utility function to get current slot
private fun getCurrentSlot(context: Context): String? { private fun getCurrentSlot(): String? {
return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let { return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let {
if (it.startsWith("_")) it.substring(1) else it if (it.startsWith("_")) it.substring(1) else it
} }

View File

@@ -0,0 +1,517 @@
package com.sukisu.ultra.ui.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
/**
* 添加路径对话框
*/
@Composable
fun AddPathDialog(
showDialog: Boolean,
onDismiss: () -> Unit,
onConfirm: (String) -> Unit,
isLoading: Boolean,
titleRes: Int,
labelRes: Int,
placeholderRes: Int
) {
var newPath by remember { mutableStateOf("") }
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
stringResource(titleRes),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
OutlinedTextField(
value = newPath,
onValueChange = { newPath = it },
label = { Text(stringResource(labelRes)) },
placeholder = { Text(stringResource(placeholderRes)) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp)
)
},
confirmButton = {
Button(
onClick = {
if (newPath.isNotBlank()) {
onConfirm(newPath.trim())
newPath = ""
}
},
enabled = newPath.isNotBlank() && !isLoading,
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.susfs_add))
}
},
dismissButton = {
TextButton(
onClick = {
onDismiss()
newPath = ""
},
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.cancel))
}
},
shape = RoundedCornerShape(12.dp)
)
}
}
/**
* 添加尝试卸载对话框
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AddTryUmountDialog(
showDialog: Boolean,
onDismiss: () -> Unit,
onConfirm: (String, Int) -> Unit,
isLoading: Boolean
) {
var newUmountPath by remember { mutableStateOf("") }
var newUmountMode by remember { mutableIntStateOf(0) }
var umountModeExpanded by remember { mutableStateOf(false) }
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
stringResource(R.string.susfs_add_try_umount),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = newUmountPath,
onValueChange = { newUmountPath = it },
label = { Text(stringResource(R.string.susfs_path_label)) },
placeholder = { Text(stringResource(R.string.susfs_path_placeholder)) },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp)
)
ExposedDropdownMenuBox(
expanded = umountModeExpanded,
onExpandedChange = { umountModeExpanded = !umountModeExpanded }
) {
OutlinedTextField(
value = if (newUmountMode == 0)
stringResource(R.string.susfs_umount_mode_normal)
else
stringResource(R.string.susfs_umount_mode_detach),
onValueChange = { },
readOnly = true,
label = { Text(stringResource(R.string.susfs_umount_mode_label)) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = umountModeExpanded) },
modifier = Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryEditable, true),
shape = RoundedCornerShape(8.dp)
)
ExposedDropdownMenu(
expanded = umountModeExpanded,
onDismissRequest = { umountModeExpanded = false }
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.susfs_umount_mode_normal)) },
onClick = {
newUmountMode = 0
umountModeExpanded = false
}
)
DropdownMenuItem(
text = { Text(stringResource(R.string.susfs_umount_mode_detach)) },
onClick = {
newUmountMode = 1
umountModeExpanded = false
}
)
}
}
}
},
confirmButton = {
Button(
onClick = {
if (newUmountPath.isNotBlank()) {
onConfirm(newUmountPath.trim(), newUmountMode)
newUmountPath = ""
newUmountMode = 0
}
},
enabled = newUmountPath.isNotBlank() && !isLoading,
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.susfs_add))
}
},
dismissButton = {
TextButton(
onClick = {
onDismiss()
newUmountPath = ""
newUmountMode = 0
},
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.cancel))
}
},
shape = RoundedCornerShape(12.dp)
)
}
}
/**
* 添加Kstat静态配置对话框
*/
@Composable
fun AddKstatStaticallyDialog(
showDialog: Boolean,
onDismiss: () -> Unit,
onConfirm: (String, String, String, String, String, String, String, String, String, String, String, String, String) -> Unit,
isLoading: Boolean
) {
var newKstatPath by remember { mutableStateOf("") }
var newKstatIno by remember { mutableStateOf("") }
var newKstatDev by remember { mutableStateOf("") }
var newKstatNlink by remember { mutableStateOf("") }
var newKstatSize by remember { mutableStateOf("") }
var newKstatAtime by remember { mutableStateOf("") }
var newKstatAtimeNsec by remember { mutableStateOf("") }
var newKstatMtime by remember { mutableStateOf("") }
var newKstatMtimeNsec by remember { mutableStateOf("") }
var newKstatCtime by remember { mutableStateOf("") }
var newKstatCtimeNsec by remember { mutableStateOf("") }
var newKstatBlocks by remember { mutableStateOf("") }
var newKstatBlksize by remember { mutableStateOf("") }
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
stringResource(R.string.add_kstat_statically_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatPath,
onValueChange = { newKstatPath = it },
label = { Text(stringResource(R.string.file_or_directory_path_label)) },
placeholder = { Text("/path/to/file_or_directory") },
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatIno,
onValueChange = { newKstatIno = it },
label = { Text("ino") },
placeholder = { Text("1234") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
OutlinedTextField(
value = newKstatDev,
onValueChange = { newKstatDev = it },
label = { Text("dev") },
placeholder = { Text("1234") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatNlink,
onValueChange = { newKstatNlink = it },
label = { Text("nlink") },
placeholder = { Text("2") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
OutlinedTextField(
value = newKstatSize,
onValueChange = { newKstatSize = it },
label = { Text("size") },
placeholder = { Text("223344") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatAtime,
onValueChange = { newKstatAtime = it },
label = { Text("atime") },
placeholder = { Text("1712592355") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
OutlinedTextField(
value = newKstatAtimeNsec,
onValueChange = { newKstatAtimeNsec = it },
label = { Text("atime_nsec") },
placeholder = { Text("0") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatMtime,
onValueChange = { newKstatMtime = it },
label = { Text("mtime") },
placeholder = { Text("1712592355") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
OutlinedTextField(
value = newKstatMtimeNsec,
onValueChange = { newKstatMtimeNsec = it },
label = { Text("mtime_nsec") },
placeholder = { Text("0") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatCtime,
onValueChange = { newKstatCtime = it },
label = { Text("ctime") },
placeholder = { Text("1712592355") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
OutlinedTextField(
value = newKstatCtimeNsec,
onValueChange = { newKstatCtimeNsec = it },
label = { Text("ctime_nsec") },
placeholder = { Text("0") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
OutlinedTextField(
value = newKstatBlocks,
onValueChange = { newKstatBlocks = it },
label = { Text("blocks") },
placeholder = { Text("16") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
OutlinedTextField(
value = newKstatBlksize,
onValueChange = { newKstatBlksize = it },
label = { Text("blksize") },
placeholder = { Text("512") },
modifier = Modifier.weight(1f),
shape = RoundedCornerShape(8.dp)
)
}
Text(
text = stringResource(R.string.hint_use_default_value),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
confirmButton = {
Button(
onClick = {
if (newKstatPath.isNotBlank()) {
onConfirm(
newKstatPath.trim(),
newKstatIno.trim().ifBlank { "default" },
newKstatDev.trim().ifBlank { "default" },
newKstatNlink.trim().ifBlank { "default" },
newKstatSize.trim().ifBlank { "default" },
newKstatAtime.trim().ifBlank { "default" },
newKstatAtimeNsec.trim().ifBlank { "default" },
newKstatMtime.trim().ifBlank { "default" },
newKstatMtimeNsec.trim().ifBlank { "default" },
newKstatCtime.trim().ifBlank { "default" },
newKstatCtimeNsec.trim().ifBlank { "default" },
newKstatBlocks.trim().ifBlank { "default" },
newKstatBlksize.trim().ifBlank { "default" }
)
// 清空所有字段
newKstatPath = ""
newKstatIno = ""
newKstatDev = ""
newKstatNlink = ""
newKstatSize = ""
newKstatAtime = ""
newKstatAtimeNsec = ""
newKstatMtime = ""
newKstatMtimeNsec = ""
newKstatCtime = ""
newKstatCtimeNsec = ""
newKstatBlocks = ""
newKstatBlksize = ""
}
},
enabled = newKstatPath.isNotBlank() && !isLoading,
shape = RoundedCornerShape(8.dp)
) {
Text("添加")
}
},
dismissButton = {
TextButton(
onClick = {
onDismiss()
// 清空所有字段
newKstatPath = ""
newKstatIno = ""
newKstatDev = ""
newKstatNlink = ""
newKstatSize = ""
newKstatAtime = ""
newKstatAtimeNsec = ""
newKstatMtime = ""
newKstatMtimeNsec = ""
newKstatCtime = ""
newKstatCtimeNsec = ""
newKstatBlocks = ""
newKstatBlksize = ""
},
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.cancel))
}
},
shape = RoundedCornerShape(12.dp)
)
}
}
/**
* 确认对话框
*/
@Composable
fun ConfirmDialog(
showDialog: Boolean,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
titleRes: Int,
messageRes: Int,
isLoading: Boolean = false,
isDestructive: Boolean = false
) {
if (showDialog) {
AlertDialog(
onDismissRequest = onDismiss,
title = {
Text(
text = stringResource(titleRes),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = { Text(stringResource(messageRes)) },
confirmButton = {
Button(
onClick = onConfirm,
enabled = !isLoading,
colors = if (isDestructive) {
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error
)
} else {
ButtonDefaults.buttonColors()
},
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(
onClick = onDismiss,
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.cancel))
}
},
shape = RoundedCornerShape(12.dp)
)
}
}

View File

@@ -0,0 +1,579 @@
package com.sukisu.ultra.ui.component
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.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.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.screen.extensions.AddKstatPathItemCard
import com.sukisu.ultra.ui.screen.extensions.EmptyStateCard
import com.sukisu.ultra.ui.screen.extensions.FeatureStatusCard
import com.sukisu.ultra.ui.screen.extensions.KstatConfigItemCard
import com.sukisu.ultra.ui.screen.extensions.PathItemCard
import com.sukisu.ultra.ui.screen.extensions.SusMountHidingControlCard
import com.sukisu.ultra.ui.util.SuSFSManager
/**
* SUS路径内容组件
*/
@Composable
fun SusPathsContent(
susPaths: Set<String>,
isLoading: Boolean,
onAddPath: () -> Unit,
onRemovePath: (String) -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (susPaths.isEmpty()) {
item {
EmptyStateCard(
message = stringResource(R.string.susfs_no_paths_configured)
)
}
} else {
items(susPaths.toList()) { path ->
PathItemCard(
path = path,
icon = Icons.Default.Folder,
onDelete = { onRemovePath(path) },
isLoading = isLoading
)
}
}
// 添加普通长按钮
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Button(
onClick = onAddPath,
modifier = Modifier
.weight(1f)
.height(48.dp),
shape = RoundedCornerShape(8.dp)
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = stringResource(R.string.susfs_add))
}
}
}
}
}
}
/**
* SUS挂载内容组件
*/
@Composable
fun SusMountsContent(
susMounts: Set<String>,
hideSusMountsForAllProcs: Boolean,
isSusVersion_1_5_8: Boolean,
isLoading: Boolean,
onAddMount: () -> Unit,
onRemoveMount: (String) -> Unit,
onToggleHideSusMountsForAllProcs: (Boolean) -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
if (isSusVersion_1_5_8) {
item {
SusMountHidingControlCard(
hideSusMountsForAllProcs = hideSusMountsForAllProcs,
isLoading = isLoading,
onToggleHiding = onToggleHideSusMountsForAllProcs
)
}
}
if (susMounts.isEmpty()) {
item {
EmptyStateCard(
message = stringResource(R.string.susfs_no_mounts_configured)
)
}
} else {
items(susMounts.toList()) { mount ->
PathItemCard(
path = mount,
icon = Icons.Default.Storage,
onDelete = { onRemoveMount(mount) },
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.susfs_add))
}
}
}
}
}
}
/**
* 尝试卸载内容组件
*/
@Composable
fun TryUmountContent(
tryUmounts: Set<String>,
isLoading: Boolean,
onAddUmount: () -> Unit,
onRunUmount: () -> Unit,
onRemoveUmount: (String) -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier
.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
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) },
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.susfs_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,
onRemoveAddKstat: (String) -> Unit,
onUpdateKstat: (String) -> Unit,
onUpdateKstatFullClone: (String) -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 说明卡片
item {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.kstat_config_description_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = stringResource(R.string.kstat_config_description_add_statically),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.kstat_config_description_add),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.kstat_config_description_update),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = stringResource(R.string.kstat_config_description_update_full_clone),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
// 静态Kstat配置列表
if (kstatConfigs.isNotEmpty()) {
item {
Text(
text = stringResource(R.string.static_kstat_config),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
items(kstatConfigs.toList()) { config ->
KstatConfigItemCard(
config = config,
onDelete = { onRemoveKstatConfig(config) },
isLoading = isLoading
)
}
}
// Add Kstat路径列表
if (addKstatPaths.isNotEmpty()) {
item {
Text(
text = stringResource(R.string.kstat_path_management),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
items(addKstatPaths.toList()) { path ->
AddKstatPathItemCard(
path = path,
onDelete = { onRemoveAddKstat(path) },
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.susfs_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.susfs_add))
}
}
}
}
}
}
/**
* 路径设置内容组件
*/
@SuppressLint("SdCardPath")
@Composable
fun PathSettingsContent(
androidDataPath: String,
onAndroidDataPathChange: (String) -> Unit,
sdcardPath: String,
onSdcardPathChange: (String) -> Unit,
isLoading: Boolean,
onSetAndroidDataPath: () -> Unit,
onSetSdcardPath: () -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Android Data路径设置
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = androidDataPath,
onValueChange = onAndroidDataPathChange,
label = { Text(stringResource(R.string.susfs_android_data_path_label)) },
placeholder = { Text("/sdcard/Android/data") },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading,
singleLine = true,
shape = RoundedCornerShape(8.dp)
)
Button(
onClick = onSetAndroidDataPath,
enabled = !isLoading && androidDataPath.isNotBlank(),
modifier = Modifier
.fillMaxWidth()
.height(40.dp),
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.susfs_set_android_data_path))
}
}
}
}
// SD卡路径设置
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedTextField(
value = sdcardPath,
onValueChange = onSdcardPathChange,
label = { Text(stringResource(R.string.susfs_sdcard_path_label)) },
placeholder = { Text("/sdcard") },
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading,
singleLine = true,
shape = RoundedCornerShape(8.dp)
)
Button(
onClick = onSetSdcardPath,
enabled = !isLoading && sdcardPath.isNotBlank(),
modifier = Modifier
.fillMaxWidth()
.height(40.dp),
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.susfs_set_sdcard_path))
}
}
}
}
}
}
/**
* 启用功能状态内容组件
*/
@Composable
fun EnabledFeaturesContent(
enabledFeatures: List<SuSFSManager.EnabledFeature>,
onRefresh: () -> Unit
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 说明卡片
item {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f)
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.susfs_enabled_features_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
if (enabledFeatures.isEmpty()) {
item {
EmptyStateCard(
message = stringResource(R.string.susfs_no_features_found)
)
}
} else {
items(enabledFeatures) { feature ->
FeatureStatusCard(
feature = feature,
onRefresh = onRefresh
)
}
}
}
}

View File

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

View File

@@ -0,0 +1,277 @@
package com.sukisu.ultra.ui.component
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.Dp
import com.sukisu.ultra.R
// 菜单项数据类
data class FabMenuItem(
val icon: ImageVector,
val labelRes: Int,
val color: Color = Color.Unspecified,
val onClick: () -> Unit
)
// 动画配置
object FabAnimationConfig {
const val ANIMATION_DURATION = 300
const val STAGGER_DELAY = 50
val BUTTON_SPACING = 72.dp
val BUTTON_SIZE = 56.dp
val SMALL_BUTTON_SIZE = 48.dp
}
@Composable
fun VerticalExpandableFab(
menuItems: List<FabMenuItem>,
modifier: Modifier = Modifier,
buttonSize: Dp = FabAnimationConfig.BUTTON_SIZE,
smallButtonSize: Dp = FabAnimationConfig.SMALL_BUTTON_SIZE,
buttonSpacing: Dp = FabAnimationConfig.BUTTON_SPACING,
animationDurationMs: Int = FabAnimationConfig.ANIMATION_DURATION,
staggerDelayMs: Int = FabAnimationConfig.STAGGER_DELAY,
mainButtonIcon: ImageVector = Icons.Filled.Add,
mainButtonExpandedIcon: ImageVector = Icons.Filled.Close,
onMainButtonClick: (() -> Unit)? = null,
) {
var isExpanded by remember { mutableStateOf(false) }
// 主按钮旋转动画
val rotationAngle by animateFloatAsState(
targetValue = if (isExpanded) 45f else 0f,
animationSpec = tween(
durationMillis = animationDurationMs,
easing = FastOutSlowInEasing
),
label = "mainButtonRotation"
)
// 主按钮缩放动画
val mainButtonScale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f,
animationSpec = tween(
durationMillis = animationDurationMs,
easing = FastOutSlowInEasing
),
label = "mainButtonScale"
)
Box(
modifier = modifier.wrapContentSize(),
contentAlignment = Alignment.BottomEnd
) {
// 子菜单按钮
menuItems.forEachIndexed { index, menuItem ->
val animatedOffsetY by animateFloatAsState(
targetValue = if (isExpanded) {
-(buttonSpacing.value * (index + 1))
} else {
0f
},
animationSpec = tween(
durationMillis = animationDurationMs,
delayMillis = if (isExpanded) {
index * staggerDelayMs
} else {
(menuItems.size - index - 1) * staggerDelayMs
},
easing = FastOutSlowInEasing
),
label = "fabOffset$index"
)
val animatedScale by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0f,
animationSpec = tween(
durationMillis = animationDurationMs,
delayMillis = if (isExpanded) {
index * staggerDelayMs + 100
} else {
(menuItems.size - index - 1) * staggerDelayMs
},
easing = FastOutSlowInEasing
),
label = "fabScale$index"
)
val animatedAlpha by animateFloatAsState(
targetValue = if (isExpanded) 1f else 0f,
animationSpec = tween(
durationMillis = animationDurationMs,
delayMillis = if (isExpanded) {
index * staggerDelayMs + 150
} else {
(menuItems.size - index - 1) * staggerDelayMs
},
easing = FastOutSlowInEasing
),
label = "fabAlpha$index"
)
// 子按钮容器(包含标签)
Row(
modifier = Modifier
.offset(y = animatedOffsetY.dp)
.scale(animatedScale)
.alpha(animatedAlpha),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
// 标签
AnimatedVisibility(
visible = isExpanded && animatedScale > 0.5f,
enter = slideInHorizontally(
initialOffsetX = { it / 2 },
animationSpec = tween(200)
) + fadeIn(animationSpec = tween(200)),
exit = slideOutHorizontally(
targetOffsetX = { it / 2 },
animationSpec = tween(150)
) + fadeOut(animationSpec = tween(150))
) {
Surface(
modifier = Modifier.padding(end = 16.dp),
shape = MaterialTheme.shapes.small,
color = MaterialTheme.colorScheme.inverseSurface,
tonalElevation = 6.dp
) {
Text(
text = stringResource(menuItem.labelRes),
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.inverseOnSurface
)
}
}
// 子按钮
SmallFloatingActionButton(
onClick = {
menuItem.onClick()
isExpanded = false
},
modifier = Modifier.size(smallButtonSize),
containerColor = if (menuItem.color != Color.Unspecified) {
menuItem.color
} else {
MaterialTheme.colorScheme.secondary
},
contentColor = if (menuItem.color != Color.Unspecified) {
if (menuItem.color == Color.Gray) Color.White
else MaterialTheme.colorScheme.onSecondary
} else {
MaterialTheme.colorScheme.onSecondary
},
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 4.dp,
pressedElevation = 6.dp
)
) {
Icon(
imageVector = menuItem.icon,
contentDescription = stringResource(menuItem.labelRes),
modifier = Modifier.size(20.dp)
)
}
}
}
// 主按钮
FloatingActionButton(
onClick = {
onMainButtonClick?.invoke()
isExpanded = !isExpanded
},
modifier = Modifier
.size(buttonSize)
.scale(mainButtonScale),
elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 6.dp,
pressedElevation = 8.dp,
hoveredElevation = 8.dp
)
) {
Icon(
imageVector = if (isExpanded) mainButtonExpandedIcon else mainButtonIcon,
contentDescription = stringResource(
if (isExpanded) R.string.collapse_menu else R.string.expand_menu
),
modifier = Modifier
.size(24.dp)
.rotate(if (mainButtonIcon == Icons.Filled.Add) rotationAngle else 0f)
)
}
}
}
// 预设菜单项
object FabMenuPresets {
fun getScrollMenuItems(
onScrollToTop: () -> Unit,
onScrollToBottom: () -> Unit
) = listOf(
FabMenuItem(
icon = Icons.Filled.KeyboardArrowDown,
labelRes = R.string.scroll_to_bottom,
onClick = onScrollToBottom
),
FabMenuItem(
icon = Icons.Filled.KeyboardArrowUp,
labelRes = R.string.scroll_to_top,
onClick = onScrollToTop
)
)
@Composable
fun getBatchActionMenuItems(
onCancel: () -> Unit,
onDeny: () -> Unit,
onAllow: () -> Unit,
onUnmountModules: () -> Unit,
onDisableUnmount: () -> Unit
) = listOf(
FabMenuItem(
icon = Icons.Filled.Close,
labelRes = R.string.cancel,
color = Color.Gray,
onClick = onCancel
),
FabMenuItem(
icon = Icons.Filled.Block,
labelRes = R.string.deny_authorization,
color = MaterialTheme.colorScheme.error,
onClick = onDeny
),
FabMenuItem(
icon = Icons.Filled.Check,
labelRes = R.string.grant_authorization,
color = MaterialTheme.colorScheme.primary,
onClick = onAllow
),
FabMenuItem(
icon = Icons.Filled.FolderOff,
labelRes = R.string.unmount_modules,
onClick = onUnmountModules
),
FabMenuItem(
icon = Icons.Filled.Folder,
labelRes = R.string.disable_unmount,
onClick = onDisableUnmount
)
)
}

View File

@@ -31,7 +31,6 @@ fun AppProfileConfig(
onValueChange = { onProfileChange(profile.copy(name = it)) } onValueChange = { onProfileChange(profile.copy(name = it)) }
) )
} }
SwitchItem( SwitchItem(
title = stringResource(R.string.profile_umount_modules), title = stringResource(R.string.profile_umount_modules),
summary = stringResource(R.string.profile_umount_modules_summary), summary = stringResource(R.string.profile_umount_modules_summary),

View File

@@ -24,6 +24,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
@@ -47,6 +48,8 @@ import com.sukisu.ultra.R
import com.sukisu.ultra.profile.Capabilities import com.sukisu.ultra.profile.Capabilities
import com.sukisu.ultra.profile.Groups import com.sukisu.ultra.profile.Groups
import com.sukisu.ultra.ui.component.rememberCustomDialog import com.sukisu.ultra.ui.component.rememberCustomDialog
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.util.isSepolicyValid import com.sukisu.ultra.ui.util.isSepolicyValid
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@@ -206,28 +209,35 @@ fun GroupsPanel(selected: List<Groups>, closeSelection: (selection: Set<Groups>)
} }
val selection = HashSet(selected) val selection = HashSet(selected)
ListDialog(
state = rememberUseCaseState(visible = true, onFinishedRequest = { MaterialTheme(
closeSelection(selection) colorScheme = MaterialTheme.colorScheme.copy(
}, onCloseRequest = { surface = MaterialTheme.colorScheme.surfaceContainerHigh
dismiss() )
}), ) {
header = Header.Default( ListDialog(
title = stringResource(R.string.profile_groups), state = rememberUseCaseState(visible = true, onFinishedRequest = {
), closeSelection(selection)
selection = ListSelection.Multiple( }, onCloseRequest = {
showCheckBoxes = true, dismiss()
options = options, }),
maxChoices = 32, // Kernel only supports 32 groups at most header = Header.Default(
) { indecies, _ -> title = stringResource(R.string.profile_groups),
// Handle selection ),
selection.clear() selection = ListSelection.Multiple(
indecies.forEach { index -> showCheckBoxes = true,
val group = groups[index] options = options,
selection.add(group) maxChoices = 32, // Kernel only supports 32 groups at most
) { indecies, _ ->
// Handle selection
selection.clear()
indecies.forEach { index ->
val group = groups[index]
selection.add(group)
}
} }
} )
) }
} }
OutlinedCard( OutlinedCard(
@@ -278,27 +288,34 @@ fun CapsPanel(
} }
val selection = HashSet(selected) val selection = HashSet(selected)
ListDialog(
state = rememberUseCaseState(visible = true, onFinishedRequest = { MaterialTheme(
closeSelection(selection) colorScheme = MaterialTheme.colorScheme.copy(
}, onCloseRequest = { surface = MaterialTheme.colorScheme.surfaceContainerHigh
dismiss() )
}), ) {
header = Header.Default( ListDialog(
title = stringResource(R.string.profile_capabilities), state = rememberUseCaseState(visible = true, onFinishedRequest = {
), closeSelection(selection)
selection = ListSelection.Multiple( }, onCloseRequest = {
showCheckBoxes = true, dismiss()
options = options }),
) { indecies, _ -> header = Header.Default(
// Handle selection title = stringResource(R.string.profile_capabilities),
selection.clear() ),
indecies.forEach { index -> selection = ListSelection.Multiple(
val group = caps[index] showCheckBoxes = true,
selection.add(group) options = options
) { indecies, _ ->
// Handle selection
selection.clear()
indecies.forEach { index ->
val group = caps[index]
selection.add(group)
}
} }
} )
) }
} }
OutlinedCard( OutlinedCard(
@@ -425,24 +442,32 @@ private fun SELinuxPanel(
) )
) )
InputDialog(
state = rememberUseCaseState(visible = true, MaterialTheme(
onFinishedRequest = { colorScheme = MaterialTheme.colorScheme.copy(
onSELinuxChange(domain, rules) surface = MaterialTheme.colorScheme.surfaceContainerHigh
},
onCloseRequest = {
dismiss()
}),
header = Header.Default(
title = stringResource(R.string.profile_selinux_context),
),
selection = InputSelection(
input = inputOptions,
onPositiveClick = { result ->
// Handle selection
},
) )
) ) {
InputDialog(
state = rememberUseCaseState(
visible = true,
onFinishedRequest = {
onSELinuxChange(domain, rules)
},
onCloseRequest = {
dismiss()
}),
header = Header.Default(
title = stringResource(R.string.profile_selinux_context),
),
selection = InputSelection(
input = inputOptions,
onPositiveClick = { result ->
// Handle selection
},
)
)
}
} }
ListItem(headlineContent = { ListItem(headlineContent = {

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,6 @@ import androidx.compose.material.icons.filled.FileUpload
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -51,7 +50,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
@@ -61,6 +59,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -75,12 +74,10 @@ import com.maxkeppeler.sheets.list.models.ListSelection
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination
import com.ramcosta.composedestinations.generated.destinations.KernelFlashScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.flash.HorizonKernelFlashProgress
import com.sukisu.ultra.flash.HorizonKernelState
import com.sukisu.ultra.flash.HorizonKernelWorker
import com.sukisu.ultra.ui.component.DialogHandle import com.sukisu.ultra.ui.component.DialogHandle
import com.sukisu.ultra.ui.component.SlotSelectionDialog import com.sukisu.ultra.ui.component.SlotSelectionDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberConfirmDialog
@@ -95,10 +92,12 @@ import com.sukisu.ultra.ui.util.isAbDevice
import com.sukisu.ultra.ui.util.isInitBoot import com.sukisu.ultra.ui.util.isInitBoot
import com.sukisu.ultra.ui.util.rootAvailable import com.sukisu.ultra.ui.util.rootAvailable
import com.sukisu.ultra.getKernelVersion import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.getCardElevation
/** /**
* @author weishu * @author ShirkNeko
* @date 2024/3/12. * @date 2025/5/31.
*/ */
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@@ -110,16 +109,10 @@ fun InstallScreen(navigator: DestinationsNavigator) {
var showRebootDialog by remember { mutableStateOf(false) } var showRebootDialog by remember { mutableStateOf(false) }
var showSlotSelectionDialog by remember { mutableStateOf(false) } var showSlotSelectionDialog by remember { mutableStateOf(false) }
var tempKernelUri by remember { mutableStateOf<Uri?>(null) } var tempKernelUri by remember { mutableStateOf<Uri?>(null) }
val horizonKernelState = remember { HorizonKernelState() }
val flashState by horizonKernelState.state.collectAsState()
val summary = stringResource(R.string.horizon_kernel_summary)
val kernelVersion = getKernelVersion() val kernelVersion = getKernelVersion()
val isGKI = kernelVersion.isGKI() val isGKI = kernelVersion.isGKI()
val isAbDevice = isAbDevice() val isAbDevice = isAbDevice()
val summary = stringResource(R.string.horizon_kernel_summary)
val onFlashComplete = {
showRebootDialog = true
}
if (showRebootDialog) { if (showRebootDialog) {
RebootDialog( RebootDialog(
@@ -145,14 +138,12 @@ fun InstallScreen(navigator: DestinationsNavigator) {
when (method) { when (method) {
is InstallMethod.HorizonKernel -> { is InstallMethod.HorizonKernel -> {
method.uri?.let { uri -> method.uri?.let { uri ->
val worker = HorizonKernelWorker( navigator.navigate(
context = context, KernelFlashScreenDestination(
state = horizonKernelState, kernelUri = uri,
slot = method.slot selectedSlot = method.slot
)
) )
worker.uri = uri
worker.setOnFlashCompleteListener(onFlashComplete)
worker.start()
} }
} }
else -> { else -> {
@@ -202,21 +193,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} }
} }
val selectLkmLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri ->
lkmSelection = LkmSelection.LkmUri(uri)
}
}
}
val onLkmUpload = {
selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/octet-stream"
})
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -224,7 +200,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
topBar = { topBar = {
TopBar( TopBar(
onBack = { navigator.popBackStack() }, onBack = { navigator.popBackStack() },
onLkmUpload = onLkmUpload,
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
}, },
@@ -241,7 +216,6 @@ fun InstallScreen(navigator: DestinationsNavigator) {
) { ) {
SelectInstallMethod( SelectInstallMethod(
isGKI = isGKI, isGKI = isGKI,
isAbDevice = isAbDevice,
onSelected = { method -> onSelected = { method ->
if (method is InstallMethod.HorizonKernel && method.uri != null) { if (method is InstallMethod.HorizonKernel && method.uri != null) {
if (isAbDevice) { if (isAbDevice) {
@@ -253,18 +227,9 @@ fun InstallScreen(navigator: DestinationsNavigator) {
} else { } else {
installMethod = method installMethod = method
} }
horizonKernelState.reset()
} }
) )
AnimatedVisibility(
visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
HorizonKernelFlashProgress(flashState)
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -273,7 +238,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
(lkmSelection as? LkmSelection.LkmUri)?.let { (lkmSelection as? LkmSelection.LkmUri)?.let {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
@@ -299,7 +264,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
if (method.slot != null) { if (method.slot != null) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
@@ -325,7 +290,7 @@ fun InstallScreen(navigator: DestinationsNavigator) {
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = installMethod != null && !flashState.isFlashing, enabled = installMethod != null,
onClick = onClickNext, onClick = onClickNext,
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
@@ -401,7 +366,6 @@ sealed class InstallMethod {
@Composable @Composable
private fun SelectInstallMethod( private fun SelectInstallMethod(
isGKI: Boolean = false, isGKI: Boolean = false,
isAbDevice: Boolean = false,
onSelected: (InstallMethod) -> Unit = {} onSelected: (InstallMethod) -> Unit = {}
) { ) {
val rootAvailable = rootAvailable() val rootAvailable = rootAvailable()
@@ -488,8 +452,8 @@ private fun SelectInstallMethod(
} }
} }
var LKMExpanded by remember { mutableStateOf(false) } var lkmExpanded by remember { mutableStateOf(false) }
var GKIExpanded by remember { mutableStateOf(false) } var gkiExpanded by remember { mutableStateOf(false) }
Column( Column(
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
@@ -498,38 +462,39 @@ private fun SelectInstallMethod(
if (isGKI) { if (isGKI) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 16.dp)
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
ListItem( MaterialTheme(
leadingContent = { colorScheme = MaterialTheme.colorScheme.copy(
Icon( surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
Icons.Filled.AutoFixHigh, )
contentDescription = null, ) {
tint = MaterialTheme.colorScheme.primary ListItem(
) leadingContent = {
}, Icon(
headlineContent = { Icons.Filled.AutoFixHigh,
Text( contentDescription = null,
stringResource(R.string.Lkm_install_methods), tint = MaterialTheme.colorScheme.primary
style = MaterialTheme.typography.titleMedium )
) },
}, headlineContent = {
modifier = Modifier.clickable { Text(
LKMExpanded = !LKMExpanded stringResource(R.string.Lkm_install_methods),
} style = MaterialTheme.typography.titleMedium
) )
},
modifier = Modifier.clickable {
lkmExpanded = !lkmExpanded
}
)
}
AnimatedVisibility( AnimatedVisibility(
visible = LKMExpanded, visible = lkmExpanded,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut() exit = shrinkVertically() + fadeOut()
) { ) {
@@ -604,38 +569,39 @@ private fun SelectInstallMethod(
if (rootAvailable) { if (rootAvailable) {
ElevatedCard( ElevatedCard(
colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation(),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 12.dp) .padding(bottom = 12.dp)
.clip(MaterialTheme.shapes.large) .clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
ListItem( MaterialTheme(
leadingContent = { colorScheme = MaterialTheme.colorScheme.copy(
Icon( surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else MaterialTheme.colorScheme.surfaceVariant
Icons.Filled.FileUpload, )
contentDescription = null, ) {
tint = MaterialTheme.colorScheme.primary ListItem(
) leadingContent = {
}, Icon(
headlineContent = { Icons.Filled.FileUpload,
Text( contentDescription = null,
stringResource(R.string.GKI_install_methods), tint = MaterialTheme.colorScheme.primary
style = MaterialTheme.typography.titleMedium )
) },
}, headlineContent = {
modifier = Modifier.clickable { Text(
GKIExpanded = !GKIExpanded stringResource(R.string.GKI_install_methods),
} style = MaterialTheme.typography.titleMedium
) )
},
modifier = Modifier.clickable {
gkiExpanded = !gkiExpanded
}
)
}
AnimatedVisibility( AnimatedVisibility(
visible = GKIExpanded, visible = gkiExpanded,
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = shrinkVertically() + fadeOut() exit = shrinkVertically() + fadeOut()
) { ) {
@@ -647,60 +613,60 @@ private fun SelectInstallMethod(
) )
) { ) {
radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option -> radioOptions.filterIsInstance<InstallMethod.HorizonKernel>().forEach { option ->
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
Surface( Surface(
color = if (option.javaClass == selectedOption?.javaClass) color = if (option.javaClass == selectedOption?.javaClass)
MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha) MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha)
else else
MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha), MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha),
shape = MaterialTheme.shapes.medium, shape = MaterialTheme.shapes.medium,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp)
.clip(MaterialTheme.shapes.medium)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 4.dp) .toggleable(
.clip(MaterialTheme.shapes.medium) value = option.javaClass == selectedOption?.javaClass,
) { onValueChange = { onClick(option) },
Row( role = Role.RadioButton,
verticalAlignment = Alignment.CenterVertically, indication = LocalIndication.current,
modifier = Modifier interactionSource = interactionSource
.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( .padding(vertical = 8.dp, horizontal = 12.dp)
modifier = Modifier ) {
.padding(start = 10.dp) RadioButton(
.weight(1f) 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(
text = stringResource(id = option.label), text = it,
style = MaterialTheme.typography.bodyLarge style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
option.summary?.let {
Text(
text = it,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} }
} }
} }
} }
}
} }
} }
} }
@@ -722,11 +688,10 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
} }
var selection by remember { mutableStateOf<String?>(null) } var selection by remember { mutableStateOf<String?>(null) }
val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
MaterialTheme( MaterialTheme(
colorScheme = MaterialTheme.colorScheme.copy( colorScheme = MaterialTheme.colorScheme.copy(
surface = backgroundColor surface = MaterialTheme.colorScheme.surfaceContainerHigh
) )
) { ) {
ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = {
@@ -749,10 +714,14 @@ fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle {
@Composable @Composable
private fun TopBar( private fun TopBar(
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onLkmUpload: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val cardColor = MaterialTheme.colorScheme.surfaceVariant val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = cardAlpha val cardAlpha = cardAlpha
TopAppBar( TopAppBar(

View File

@@ -5,9 +5,13 @@ import android.content.Intent
import android.util.Log import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -38,6 +42,7 @@ import java.io.FileInputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.* import java.net.*
import android.app.Activity import android.app.Activity
import androidx.compose.ui.res.painterResource
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
/** /**
@@ -57,6 +62,9 @@ fun KpmScreen(
val snackBarHost = remember { SnackbarHostState() } val snackBarHost = remember { SnackbarHostState() }
val confirmDialog = rememberConfirmDialog() val confirmDialog = rememberConfirmDialog()
val listState = rememberLazyListState()
val fabVisible by rememberFabVisibilityState(listState)
val moduleConfirmContentMap = viewModel.moduleList.associate { module -> val moduleConfirmContentMap = viewModel.moduleList.associate { module ->
val moduleFileName = module.id val moduleFileName = module.id
module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName) module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName)
@@ -112,7 +120,6 @@ fun KpmScreen(
Text( Text(
text = kpmInstallMode, text = kpmInstallMode,
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
) )
}, },
text = { text = {
@@ -121,7 +128,6 @@ fun KpmScreen(
Text( Text(
text = stringResource(R.string.kpm_install_mode_description, it), text = stringResource(R.string.kpm_install_mode_description, it),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -146,9 +152,6 @@ fun KpmScreen(
} }
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Download, imageVector = Icons.Filled.Download,
@@ -176,9 +179,6 @@ fun KpmScreen(
} }
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Inventory, imageVector = Icons.Filled.Inventory,
@@ -209,7 +209,6 @@ fun KpmScreen(
} }
} }
}, },
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge shape = MaterialTheme.shapes.extraLarge
) )
} }
@@ -292,38 +291,34 @@ fun KpmScreen(
) )
}, },
floatingActionButton = { floatingActionButton = {
ExtendedFloatingActionButton( AnimatedFab(visible = fabVisible) {
onClick = { FloatingActionButton(
selectPatchLauncher.launch( contentColor = MaterialTheme.colorScheme.onPrimary,
Intent(Intent.ACTION_GET_CONTENT).apply { containerColor = MaterialTheme.colorScheme.primary,
type = "application/octet-stream" onClick = {
} selectPatchLauncher.launch(
) Intent(Intent.ACTION_GET_CONTENT).apply {
}, type = "application/octet-stream"
icon = { }
Icon( )
imageVector = Icons.Filled.Add, },
contentDescription = stringResource(R.string.kpm_install), content = {
) Icon(
}, painter = painterResource(id = R.drawable.package_import),
text = { contentDescription = null
Text( )
text = stringResource(R.string.kpm_install), }
color = MaterialTheme.colorScheme.onPrimaryContainer )
) }
},
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
expanded = true,
)
}, },
contentWindowInsets = WindowInsets.safeDrawing.only(
WindowInsetsSides.Top + WindowInsetsSides.Horizontal
),
snackbarHost = { SnackbarHost(snackBarHost) } snackbarHost = { SnackbarHost(snackBarHost) }
) { padding -> ) { padding ->
Column(modifier = Modifier.padding(padding)) { Column(modifier = Modifier.padding(padding)) {
if (!isNoticeClosed) { if (!isNoticeClosed) {
Card( Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp) .padding(16.dp)
@@ -348,7 +343,6 @@ fun KpmScreen(
text = stringResource(R.string.kernel_module_notice), text = stringResource(R.string.kernel_module_notice),
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer
) )
IconButton( IconButton(
@@ -357,9 +351,6 @@ fun KpmScreen(
sharedPreferences.edit { putBoolean("is_notice_closed", true) } sharedPreferences.edit { putBoolean("is_notice_closed", true) }
}, },
modifier = Modifier.size(24.dp), modifier = Modifier.size(24.dp),
colors = IconButtonDefaults.iconButtonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Close, imageVector = Icons.Filled.Close,
@@ -391,12 +382,12 @@ fun KpmScreen(
stringResource(R.string.kpm_empty), stringResource(R.string.kpm_empty),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
} else { } else {
LazyColumn( LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(16.dp)
@@ -585,7 +576,6 @@ private fun KpmModuleItem(
Text( Text(
text = stringResource(R.string.kpm_control), text = stringResource(R.string.kpm_control),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface
) )
}, },
text = { text = {
@@ -595,20 +585,14 @@ private fun KpmModuleItem(
label = { label = {
Text( Text(
text = stringResource(R.string.kpm_args), text = stringResource(R.string.kpm_args),
color = MaterialTheme.colorScheme.primary
) )
}, },
placeholder = { placeholder = {
Text( Text(
text = module.args, text = module.args,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
) )
}, },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
unfocusedBorderColor = MaterialTheme.colorScheme.outline
)
) )
}, },
confirmButton = { confirmButton = {
@@ -627,7 +611,6 @@ private fun KpmModuleItem(
) { ) {
Text( Text(
text = stringResource(R.string.confirm), text = stringResource(R.string.confirm),
color = MaterialTheme.colorScheme.primary
) )
} }
}, },
@@ -635,26 +618,16 @@ private fun KpmModuleItem(
TextButton(onClick = { viewModel.hideInputDialog() }) { TextButton(onClick = { viewModel.hideInputDialog() }) {
Text( Text(
text = stringResource(R.string.cancel), text = stringResource(R.string.cancel),
color = MaterialTheme.colorScheme.primary
) )
} }
}, },
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = MaterialTheme.shapes.extraLarge shape = MaterialTheme.shapes.extraLarge
) )
} }
Card( Card(
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), elevation = getCardElevation()
modifier = Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.large)
.shadow(
elevation = cardElevation,
shape = MaterialTheme.shapes.large,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)
)
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp) modifier = Modifier.padding(20.dp)
@@ -668,7 +641,6 @@ private fun KpmModuleItem(
Text( Text(
text = module.name, text = module.name,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface
) )
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
@@ -676,19 +648,16 @@ private fun KpmModuleItem(
Text( Text(
text = "${stringResource(R.string.kpm_version)}: ${module.version}", text = "${stringResource(R.string.kpm_version)}: ${module.version}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = "${stringResource(R.string.kpm_author)}: ${module.author}", text = "${stringResource(R.string.kpm_author)}: ${module.author}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Text( Text(
text = "${stringResource(R.string.kpm_args)}: ${module.args}", text = "${stringResource(R.string.kpm_args)}: ${module.args}",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
@@ -698,7 +667,6 @@ private fun KpmModuleItem(
Text( Text(
text = module.description, text = module.description,
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
@@ -711,10 +679,6 @@ private fun KpmModuleItem(
onClick = { viewModel.showInputDialog(module.id) }, onClick = { viewModel.showInputDialog(module.id) },
enabled = module.hasAction, enabled = module.hasAction,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant
)
) { ) {
Icon( Icon(
imageVector = Icons.Filled.Settings, imageVector = Icons.Filled.Settings,

View File

@@ -13,7 +13,6 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
@@ -32,6 +31,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -53,18 +53,22 @@ import kotlinx.coroutines.withContext
import com.sukisu.ultra.BuildConfig import com.sukisu.ultra.BuildConfig
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.*
import com.sukisu.ultra.ui.component.* import com.sukisu.ultra.ui.component.*
import com.sukisu.ultra.ui.theme.* import com.sukisu.ultra.ui.theme.*
import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha
import com.sukisu.ultra.ui.theme.CardConfig.cardElevation
import com.sukisu.ultra.ui.util.LocalSnackbarHost import com.sukisu.ultra.ui.util.LocalSnackbarHost
import com.sukisu.ultra.ui.util.getBugreportFile import com.sukisu.ultra.ui.util.getBugreportFile
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.KsuIsValid
import com.dergoogler.mmrl.platform.Platform
/**
* @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) @OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph> @Destination<RootGraph>
@@ -72,12 +76,18 @@ import com.dergoogler.mmrl.platform.Platform
fun SettingScreen(navigator: DestinationsNavigator) { fun SettingScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current 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( Scaffold(
// containerColor = MaterialTheme.colorScheme.surfaceBright,
topBar = { topBar = {
TopBar( TopBar(scrollBehavior = scrollBehavior)
scrollBehavior = scrollBehavior
)
}, },
snackbarHost = { SnackbarHost(snackBarHost) }, snackbarHost = { SnackbarHost(snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
@@ -111,31 +121,16 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
} }
// 配置 // 配置卡片
Card( SettingsGroupCard(
modifier = Modifier title = stringResource(R.string.configuration),
.fillMaxWidth() content = {
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.configuration),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
// 配置文件模板入口 // 配置文件模板入口
val profileTemplate = stringResource(id = R.string.settings_profile_template)
KsuIsValid { KsuIsValid {
SettingItem( SettingItem(
icon = Icons.Filled.Fence, icon = Icons.Filled.Fence,
title = profileTemplate, title = stringResource(R.string.settings_profile_template),
summary = stringResource(id = R.string.settings_profile_template_summary), summary = stringResource(R.string.settings_profile_template_summary),
onClick = { onClick = {
navigator.navigate(AppProfileTemplateScreenDestination) navigator.navigate(AppProfileTemplateScreenDestination)
} }
@@ -148,244 +143,176 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
KsuIsValid { KsuIsValid {
SwitchSettingItem( SwitchItem(
icon = Icons.Filled.FolderDelete, icon = Icons.Filled.FolderDelete,
title = stringResource(id = R.string.settings_umount_modules_default), title = stringResource(R.string.settings_umount_modules_default),
summary = stringResource(id = R.string.settings_umount_modules_default_summary), summary = stringResource(R.string.settings_umount_modules_default_summary),
checked = umountChecked, checked = umountChecked,
onCheckedChange = { onCheckedChange = { enabled ->
if (Natives.setDefaultUmountModules(it)) { if (Natives.setDefaultUmountModules(enabled)) {
umountChecked = it umountChecked = enabled
} }
} }
) )
} }
// SU 禁用开关(仅在兼容版本显示) // SU 禁用开关
KsuIsValid { KsuIsValid {
if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) {
var isSuDisabled by rememberSaveable { var isSuDisabled by rememberSaveable {
mutableStateOf(!Natives.isSuEnabled()) mutableStateOf(!Natives.isSuEnabled())
} }
SwitchSettingItem( SwitchItem(
icon = Icons.Filled.RemoveModerator, icon = Icons.Filled.RemoveModerator,
title = stringResource(id = R.string.settings_disable_su), title = stringResource(R.string.settings_disable_su),
summary = stringResource(id = R.string.settings_disable_su_summary), summary = stringResource(R.string.settings_disable_su_summary),
checked = isSuDisabled, checked = isSuDisabled,
onCheckedChange = { checked -> onCheckedChange = { enabled ->
val shouldEnable = !checked val shouldEnable = !enabled
if (Natives.setSuEnabled(shouldEnable)) { if (Natives.setSuEnabled(shouldEnable)) {
isSuDisabled = !shouldEnable isSuDisabled = enabled
} }
} }
) )
} }
} }
} }
} )
// 应用设置
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.app_settings),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
// 应用设置卡片
SettingsGroupCard(
title = stringResource(R.string.app_settings),
content = {
// 更新检查开关 // 更新检查开关
var checkUpdate by rememberSaveable { var checkUpdate by rememberSaveable {
mutableStateOf( mutableStateOf(prefs.getBoolean("check_update", true))
prefs.getBoolean("check_update", true)
)
} }
SwitchSettingItem( SwitchItem(
icon = Icons.Filled.Update, icon = Icons.Filled.Update,
title = stringResource(id = R.string.settings_check_update), title = stringResource(R.string.settings_check_update),
summary = stringResource(id = R.string.settings_check_update_summary), summary = stringResource(R.string.settings_check_update_summary),
checked = checkUpdate, checked = checkUpdate,
onCheckedChange = { onCheckedChange = { enabled ->
prefs.edit {putBoolean("check_update", it) } prefs.edit { putBoolean("check_update", enabled) }
checkUpdate = it checkUpdate = enabled
} }
) )
// Web调试开关 // WebUI引擎选择
var enableWebDebugging by rememberSaveable {
mutableStateOf(
prefs.getBoolean("enable_web_debugging", false)
)
}
KsuIsValid { KsuIsValid {
SwitchSettingItem( WebUIEngineSelector(
icon = Icons.Filled.DeveloperMode, selectedEngine = selectedEngine,
title = stringResource(id = R.string.enable_web_debugging), onEngineSelected = { engine ->
summary = stringResource(id = R.string.enable_web_debugging_summary), selectedEngine = engine
checked = enableWebDebugging, prefs.edit { putString("webui_engine", engine) }
onCheckedChange = {
prefs.edit { putBoolean("enable_web_debugging", it) }
enableWebDebugging = it
} }
) )
} }
// Web X 开关 // Web调试和Web X Eruda 开关
var useWebUIX by rememberSaveable { var enableWebDebugging by rememberSaveable {
mutableStateOf( mutableStateOf(prefs.getBoolean("enable_web_debugging", false))
prefs.getBoolean("use_webuix", false)
)
} }
KsuIsValid { var useWebUIXEruda by rememberSaveable {
SwitchItem( mutableStateOf(prefs.getBoolean("use_webuix_eruda", false))
beta = true,
enabled = Platform.isAlive,
icon = Icons.Filled.WebAsset,
title = stringResource(id = R.string.use_webuix),
summary = stringResource(id = R.string.use_webuix_summary),
checked = useWebUIX
) {
prefs.edit { putBoolean("use_webuix", it) }
useWebUIX = it
}
} }
// Web X Eruda 开关
var useWebUIXEruda by rememberSaveable {
mutableStateOf(
prefs.getBoolean("use_webuix_eruda", false)
)
}
KsuIsValid { 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( AnimatedVisibility(
visible = useWebUIX && enableWebDebugging, visible = enableWebDebugging && selectedEngine == "wx",
enter = fadeIn() + expandVertically(), enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically() exit = fadeOut() + shrinkVertically()
) { ) {
SwitchItem( SwitchItem(
beta = true,
enabled = Platform.isAlive && useWebUIX && enableWebDebugging,
icon = Icons.Filled.FormatListNumbered, icon = Icons.Filled.FormatListNumbered,
title = stringResource(id = R.string.use_webuix_eruda), title = stringResource(R.string.use_webuix_eruda),
summary = stringResource(id = R.string.use_webuix_eruda_summary), summary = stringResource(R.string.use_webuix_eruda_summary),
checked = useWebUIXEruda checked = useWebUIXEruda,
) { onCheckedChange = { enabled ->
prefs.edit { putBoolean("use_webuix_eruda", it) } prefs.edit { putBoolean("use_webuix_eruda", enabled) }
useWebUIXEruda = it useWebUIXEruda = enabled
} }
)
} }
} }
// 更多设置 // 更多设置
SettingItem( SettingItem(
icon = Icons.Filled.Settings, icon = Icons.Filled.Settings,
title = stringResource(id = R.string.more_settings), title = stringResource(R.string.more_settings),
summary = stringResource(id = R.string.more_settings), summary = stringResource(R.string.more_settings),
onClick = { onClick = {
navigator.navigate(MoreSettingsScreenDestination) navigator.navigate(MoreSettingsScreenDestination)
} }
) )
} }
} )
// 工具
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.tools),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
// 工具卡片
SettingsGroupCard(
title = stringResource(R.string.tools),
content = {
var showBottomsheet by remember { mutableStateOf(false) } var showBottomsheet by remember { mutableStateOf(false) }
SettingItem( SettingItem(
icon = Icons.Filled.BugReport, icon = Icons.Filled.BugReport,
title = stringResource(id = R.string.send_log), title = stringResource(R.string.send_log),
onClick = { onClick = {
showBottomsheet = true showBottomsheet = true
} }
) )
if (showBottomsheet) { if (showBottomsheet) {
ModalBottomSheet( LogBottomSheet(
onDismissRequest = { showBottomsheet = false }, onDismiss = { showBottomsheet = false },
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, onSaveLog = {
) { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
Row( val current = LocalDateTime.now().format(formatter)
modifier = Modifier exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
.fillMaxWidth() showBottomsheet = false
.padding(16.dp), },
horizontalArrangement = Arrangement.SpaceEvenly onShareLog = {
) { scope.launch {
LogActionButton( val bugreport = loadingDialog.withLoading {
icon = Icons.Filled.Save, withContext(Dispatchers.IO) {
text = stringResource(R.string.save_log), getBugreportFile(context)
onClick = {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm")
val current = LocalDateTime.now().format(formatter)
exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz")
showBottomsheet = false
}
)
LogActionButton(
icon = Icons.Filled.Share,
text = stringResource(R.string.send_log),
onClick = {
scope.launch {
val bugreport = loadingDialog.withLoading {
withContext(Dispatchers.IO) {
getBugreportFile(context)
}
}
val uri: Uri =
FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileprovider",
bugreport
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
setDataAndType(uri, "application/gzip")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(
shareIntent,
context.getString(R.string.send_log)
)
)
showBottomsheet = false
} }
} }
)
val uri = FileProvider.getUriForFile(
context,
"${BuildConfig.APPLICATION_ID}.fileprovider",
bugreport
)
val shareIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
setDataAndType(uri, "application/gzip")
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(
Intent.createChooser(
shareIntent,
context.getString(R.string.send_log)
)
)
showBottomsheet = false
}
} }
Spacer(modifier = Modifier.height(16.dp)) )
}
} }
val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode
@@ -395,26 +322,12 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
} }
} }
} )
// 设置分组卡片 - 关于
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha)
),
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation)
) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = stringResource(R.string.about),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
)
// 关于卡片
SettingsGroupCard(
title = stringResource(R.string.about),
content = {
SettingItem( SettingItem(
icon = Icons.Filled.Info, icon = Icons.Filled.Info,
title = stringResource(R.string.about), title = stringResource(R.string.about),
@@ -423,13 +336,128 @@ fun SettingScreen(navigator: DestinationsNavigator) {
} }
) )
} }
} )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(SPACING_LARGE))
} }
} }
} }
@Composable
private fun SettingsGroupCard(
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow),
elevation = getCardElevation()
) {
Column(
modifier = Modifier.padding(vertical = SPACING_MEDIUM)
) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
color = MaterialTheme.colorScheme.primary
)
content()
}
}
}
@Composable
private fun WebUIEngineSelector(
selectedEngine: String,
onEngineSelected: (String) -> Unit
) {
var showDialog by remember { mutableStateOf(false) }
val engineOptions = listOf(
"default" to stringResource(R.string.engine_auto_select),
"wx" to stringResource(R.string.engine_force_webuix),
"ksu" to stringResource(R.string.engine_force_ksu)
)
SettingItem(
icon = Icons.Filled.WebAsset,
title = stringResource(R.string.use_webuix),
summary = engineOptions.find { it.first == selectedEngine }?.second
?: stringResource(R.string.engine_auto_select),
onClick = { showDialog = true }
)
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text(stringResource(R.string.use_webuix)) },
text = {
Column {
engineOptions.forEach { (value, label) ->
Row(
modifier = Modifier
.fillMaxWidth()
.clickable {
onEngineSelected(value)
showDialog = false
}
.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedEngine == value,
onClick = null
)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(text = label)
}
}
}
},
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text(stringResource(R.string.cancel))
}
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun LogBottomSheet(
onDismiss: () -> Unit,
onSaveLog: () -> Unit,
onShareLog: () -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
horizontalArrangement = Arrangement.SpaceEvenly
) {
LogActionButton(
icon = Icons.Filled.Save,
text = stringResource(R.string.save_log),
onClick = onSaveLog
)
LogActionButton(
icon = Icons.Filled.Share,
text = stringResource(R.string.send_log),
onClick = onShareLog
)
}
Spacer(modifier = Modifier.height(SPACING_LARGE))
}
}
@Composable @Composable
fun LogActionButton( fun LogActionButton(
icon: ImageVector, icon: ImageVector,
@@ -440,7 +468,7 @@ fun LogActionButton(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(8.dp) .padding(SPACING_MEDIUM)
) { ) {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@@ -456,11 +484,10 @@ fun LogActionButton(
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp)
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium
color = MaterialTheme.colorScheme.onSurface
) )
} }
} }
@@ -476,33 +503,31 @@ fun SettingItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = SPACING_LARGE, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.Top
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier modifier = Modifier
.padding(end = 16.dp) .padding(end = SPACING_LARGE)
.size(24.dp) .size(24.dp)
) )
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium
color = MaterialTheme.colorScheme.onSurface
) )
if (summary != null) { if (summary != null) {
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text( Text(
text = summary, text = summary,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
Icon( Icon(
imageVector = Icons.Filled.ChevronRight, imageVector = Icons.Filled.ChevronRight,
contentDescription = null, contentDescription = null,
@@ -513,7 +538,7 @@ fun SettingItem(
} }
@Composable @Composable
fun SwitchSettingItem( fun SwitchItem(
icon: ImageVector, icon: ImageVector,
title: String, title: String,
summary: String? = null, summary: String? = null,
@@ -524,44 +549,34 @@ fun SwitchSettingItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { onCheckedChange(!checked) } .clickable { onCheckedChange(!checked) }
.padding(horizontal = 16.dp, vertical = 12.dp), .padding(horizontal = SPACING_LARGE, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.Top
) { ) {
Icon( Icon(
imageVector = icon, imageVector = icon,
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier modifier = Modifier
.padding(end = 16.dp) .padding(end = SPACING_LARGE)
.size(24.dp) .size(24.dp)
) )
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium
color = MaterialTheme.colorScheme.onSurface
) )
if (summary != null) { if (summary != null) {
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text( Text(
text = summary, text = summary,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium
color = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
} }
Switch( Switch(
checked = checked, checked = checked,
onCheckedChange = onCheckedChange, onCheckedChange = onCheckedChange
colors = SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
checkedTrackColor = MaterialTheme.colorScheme.primary,
checkedIconColor = MaterialTheme.colorScheme.primary,
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant,
uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant
)
) )
} }
} }
@@ -645,97 +660,123 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
) )
} }
var selection = UninstallType.NONE var selectedOption by remember { mutableStateOf<UninstallType?>(null) }
val cardColor = if (!ThemeConfig.useDynamicColor) {
ThemeConfig.currentTheme.ButtonContrast
} else {
MaterialTheme.colorScheme.surfaceContainerHigh
}
AlertDialog( MaterialTheme(
onDismissRequest = { colorScheme = MaterialTheme.colorScheme.copy(
dismiss() surface = MaterialTheme.colorScheme.surfaceContainerHigh
}, )
title = { ) {
Text( AlertDialog(
text = stringResource(R.string.settings_uninstall), onDismissRequest = {
style = MaterialTheme.typography.headlineSmall, dismiss()
color = MaterialTheme.colorScheme.onSurface },
) title = {
}, Text(
text = { text = stringResource(R.string.settings_uninstall),
Column( style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(vertical = 8.dp), )
verticalArrangement = Arrangement.spacedBy(16.dp) },
) { text = {
listOptions.forEachIndexed { index, option -> Column(
Row( modifier = Modifier.padding(vertical = 8.dp),
modifier = Modifier verticalArrangement = Arrangement.spacedBy(16.dp)
.fillMaxWidth() ) {
.clip(MaterialTheme.shapes.medium) options.forEachIndexed { index, option ->
.clickable { val isSelected = selectedOption == option
selection = options[index] val backgroundColor = if (isSelected)
} MaterialTheme.colorScheme.primaryContainer
.padding(vertical = 12.dp, horizontal = 8.dp), else
verticalAlignment = Alignment.CenterVertically Color.Transparent
) { val contentColor = if (isSelected)
Icon( MaterialTheme.colorScheme.onPrimaryContainer
imageVector = options[index].icon, else
contentDescription = null, MaterialTheme.colorScheme.onSurface
tint = MaterialTheme.colorScheme.primary,
Row(
modifier = Modifier modifier = Modifier
.padding(end = 16.dp) .fillMaxWidth()
.size(24.dp) .clip(MaterialTheme.shapes.medium)
) .background(backgroundColor)
Column { .clickable {
Text( selectedOption = option
text = option.titleText, }
style = MaterialTheme.typography.titleMedium, .padding(vertical = 12.dp, horizontal = 8.dp),
color = MaterialTheme.colorScheme.onSurface verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = option.icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(end = 16.dp)
.size(24.dp)
) )
option.subtitleText?.let { Column(
modifier = Modifier.weight(1f)
) {
Text( Text(
text = it, text = listOptions[index].titleText,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant )
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 = {
confirmButton = { Button(
TextButton( onClick = {
onClick = { selectedOption?.let { onSelected(it) }
if (selection != UninstallType.NONE) { dismiss()
onSelected(selection) },
enabled = selectedOption != null,
) {
Text(
text = stringResource(android.R.string.ok)
)
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
} }
dismiss() ) {
Text(
text = stringResource(android.R.string.cancel),
)
} }
) { },
Text( shape = MaterialTheme.shapes.extraLarge,
text = stringResource(android.R.string.ok), tonalElevation = 4.dp
color = MaterialTheme.colorScheme.primary )
) }
}
},
dismissButton = {
TextButton(
onClick = {
dismiss()
}
) {
Text(
text = stringResource(android.R.string.cancel),
color = MaterialTheme.colorScheme.primary
)
}
},
containerColor = cardColor,
shape = MaterialTheme.shapes.extraLarge,
tonalElevation = 4.dp
)
} }
} }
@@ -744,14 +785,12 @@ fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle {
private fun TopBar( private fun TopBar(
scrollBehavior: TopAppBarScrollBehavior? = null scrollBehavior: TopAppBarScrollBehavior? = null
) { ) {
val systemIsDark = isSystemInDarkTheme() val colorScheme = MaterialTheme.colorScheme
val cardColor = MaterialTheme.colorScheme.surfaceVariant val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
val cardAlpha = if (ThemeConfig.customBackgroundUri != null) { colorScheme.surfaceContainerLow
cardAlpha
} else { } else {
if (systemIsDark) 0.8f else 1f colorScheme.background
} }
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
@@ -766,4 +805,4 @@ private fun TopBar(
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,581 @@
package com.sukisu.ultra.ui.screen.extensions
import android.annotation.SuppressLint
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Update
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.util.SuSFSManager
import kotlinx.coroutines.launch
/**
* 空状态显示组件
*/
@Composable
fun EmptyStateCard(
message: String,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.2f)
),
shape = RoundedCornerShape(12.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
Text(
text = message,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
}
}
/**
* 路径项目卡片组件
*/
@Composable
fun PathItemCard(
path: String,
icon: ImageVector,
onDelete: () -> Unit,
isLoading: Boolean = false,
additionalInfo: String? = null
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 1.dp),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = path,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
if (additionalInfo != null) {
Text(
text = additionalInfo,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
IconButton(
onClick = onDelete,
enabled = !isLoading,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
}
}
}
}
/**
* Kstat配置项目卡片组件
*/
@Composable
fun KstatConfigItemCard(
config: String,
onDelete: () -> Unit,
isLoading: Boolean = false
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 1.dp),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
val parts = config.split("|")
if (parts.isNotEmpty()) {
Text(
text = parts[0], // 路径
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
if (parts.size > 1) {
Text(
text = "${parts.drop(1).joinToString(" ")}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
} else {
Text(
text = config,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
}
}
}
IconButton(
onClick = onDelete,
enabled = !isLoading,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
}
}
}
}
/**
* Add Kstat路径项目卡片组件
*/
@Composable
fun AddKstatPathItemCard(
path: String,
onDelete: () -> Unit,
onUpdate: () -> Unit,
onUpdateFullClone: () -> Unit,
isLoading: Boolean = false
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 1.dp),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Folder,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = path,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
IconButton(
onClick = onUpdate,
enabled = !isLoading,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Update,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary,
modifier = Modifier.size(16.dp)
)
}
IconButton(
onClick = onUpdateFullClone,
enabled = !isLoading,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.PlayArrow,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary,
modifier = Modifier.size(16.dp)
)
}
IconButton(
onClick = onDelete,
enabled = !isLoading,
modifier = Modifier.size(32.dp)
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
}
}
}
}
}
/**
* 启用功能状态卡片组件
*/
@Composable
fun FeatureStatusCard(
feature: SuSFSManager.EnabledFeature,
onRefresh: (() -> Unit)? = null,
@SuppressLint("ModifierParameter") modifier: Modifier = Modifier
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
// 日志配置对话框状态
var showLogConfigDialog by remember { mutableStateOf(false) }
var logEnabled by remember { mutableStateOf(SuSFSManager.getEnableLogState(context)) }
// 日志配置对话框
if (showLogConfigDialog) {
AlertDialog(
onDismissRequest = { showLogConfigDialog = false },
title = {
Text(
text = stringResource(R.string.susfs_log_config_title),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = stringResource(R.string.susfs_log_config_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.susfs_enable_log_label),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
Switch(
checked = logEnabled,
onCheckedChange = { logEnabled = it }
)
}
}
},
confirmButton = {
Button(
onClick = {
coroutineScope.launch {
if (SuSFSManager.setEnableLog(context, logEnabled)) {
onRefresh?.invoke()
}
showLogConfigDialog = false
}
},
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.susfs_apply))
}
},
dismissButton = {
TextButton(
onClick = {
// 恢复原始状态
logEnabled = SuSFSManager.getEnableLogState(context)
showLogConfigDialog = false
},
shape = RoundedCornerShape(8.dp)
) {
Text(stringResource(R.string.cancel))
}
},
shape = RoundedCornerShape(12.dp)
)
}
Card(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 1.dp)
.then(
if (feature.canConfigure) {
Modifier.clickable {
// 更新当前状态
logEnabled = SuSFSManager.getEnableLogState(context)
showLogConfigDialog = true
}
} else {
Modifier
}
),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(12.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = feature.name,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium
)
if (feature.canConfigure) {
Text(
text = stringResource(R.string.susfs_feature_configurable),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 状态标签
Surface(
shape = RoundedCornerShape(6.dp),
color = when {
feature.isEnabled -> MaterialTheme.colorScheme.primary
else -> Color.Gray
}
) {
Text(
text = feature.statusText,
style = MaterialTheme.typography.labelLarge,
color = when {
feature.isEnabled -> MaterialTheme.colorScheme.onPrimary
else -> Color.White
},
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp)
)
}
}
}
}
}
/**
* SUS挂载隐藏控制卡片组件
*/
@Composable
fun SusMountHidingControlCard(
hideSusMountsForAllProcs: Boolean,
isLoading: Boolean,
onToggleHiding: (Boolean) -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
// 标题行
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = if (hideSusMountsForAllProcs) Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.susfs_hide_mounts_control_title),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface
)
}
// 描述文本
Text(
text = stringResource(R.string.susfs_hide_mounts_control_description),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 16.sp
)
// 控制开关行
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = stringResource(R.string.susfs_hide_mounts_for_all_procs_label),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (hideSusMountsForAllProcs) {
stringResource(R.string.susfs_hide_mounts_for_all_procs_enabled_description)
} else {
stringResource(R.string.susfs_hide_mounts_for_all_procs_disabled_description)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 14.sp
)
}
Switch(
checked = hideSusMountsForAllProcs,
onCheckedChange = onToggleHiding,
enabled = !isLoading
)
}
// 当前设置显示
Text(
text = stringResource(
R.string.susfs_hide_mounts_current_setting,
if (hideSusMountsForAllProcs) {
stringResource(R.string.susfs_hide_mounts_setting_all)
} else {
stringResource(R.string.susfs_hide_mounts_setting_non_ksu)
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Medium
)
// 建议文本
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
),
shape = RoundedCornerShape(8.dp)
) {
Text(
text = stringResource(R.string.susfs_hide_mounts_recommendation),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
lineHeight = 14.sp,
modifier = Modifier.padding(12.dp)
)
}
}
}
}

View File

@@ -5,21 +5,23 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
object CardConfig { object CardConfig {
val settingElevation: Dp = 4.dp // 卡片透明度
val customBackgroundElevation: Dp = 0.dp var cardAlpha by mutableFloatStateOf(1f)
// 卡片亮度
var cardAlpha by mutableStateOf(1f) var cardDim by mutableFloatStateOf(0f)
var cardElevation by mutableStateOf(settingElevation) // 卡片阴影
var cardElevation by mutableStateOf(0.dp)
var isShadowEnabled by mutableStateOf(true) var isShadowEnabled by mutableStateOf(true)
var isCustomAlphaSet by mutableStateOf(false) var isCustomAlphaSet by mutableStateOf(false)
var isCustomDimSet by mutableStateOf(false)
var isUserDarkModeEnabled by mutableStateOf(false) var isUserDarkModeEnabled by mutableStateOf(false)
var isUserLightModeEnabled by mutableStateOf(false) var isUserLightModeEnabled by mutableStateOf(false)
var isCustomBackgroundEnabled by mutableStateOf(false) var isCustomBackgroundEnabled by mutableStateOf(false)
@@ -31,9 +33,11 @@ object CardConfig {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit().apply { prefs.edit().apply {
putFloat("card_alpha", cardAlpha) putFloat("card_alpha", cardAlpha)
putFloat("card_dim", cardDim)
putBoolean("custom_background_enabled", isCustomBackgroundEnabled) putBoolean("custom_background_enabled", isCustomBackgroundEnabled)
putBoolean("is_shadow_enabled", isShadowEnabled) putBoolean("is_shadow_enabled", isShadowEnabled)
putBoolean("is_custom_alpha_set", isCustomAlphaSet) putBoolean("is_custom_alpha_set", isCustomAlphaSet)
putBoolean("is_custom_dim_set", isCustomDimSet)
putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled) putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled)
putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled) putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled)
apply() apply()
@@ -46,9 +50,11 @@ object CardConfig {
fun load(context: Context) { fun load(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
cardAlpha = prefs.getFloat("card_alpha", 1f) cardAlpha = prefs.getFloat("card_alpha", 1f)
cardDim = prefs.getFloat("card_dim", 0f)
isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false) isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false)
isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true) isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true)
isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false) isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false)
isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false)
isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false) isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false)
isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false) isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false)
updateShadowEnabled(isShadowEnabled) updateShadowEnabled(isShadowEnabled)
@@ -59,22 +65,19 @@ object CardConfig {
*/ */
fun updateShadowEnabled(enabled: Boolean) { fun updateShadowEnabled(enabled: Boolean) {
isShadowEnabled = enabled isShadowEnabled = enabled
cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) { cardElevation = 0.dp
customBackgroundElevation
} else if (enabled) {
settingElevation
} else {
customBackgroundElevation
}
} }
/** /**
* 设置深色模式默认值 * 设置主题模式默认值
*/ */
fun setDarkModeDefaults() { fun setThemeDefaults(isDarkMode: Boolean) {
if (!isCustomAlphaSet) { if (!isCustomAlphaSet) {
cardAlpha = 1f cardAlpha = 1f
} }
if (!isCustomDimSet) {
cardDim = if (isDarkMode) 0.5f else 0f
}
updateShadowEnabled(isShadowEnabled) updateShadowEnabled(isShadowEnabled)
} }
} }
@@ -88,6 +91,19 @@ fun getCardColors(originalColor: Color) = CardDefaults.cardColors(
contentColor = determineContentColor(originalColor) contentColor = determineContentColor(originalColor)
) )
/**
* 获取卡片阴影配置
*/
@Composable
fun getCardElevation() = CardDefaults.cardElevation(
defaultElevation = CardConfig.cardElevation,
pressedElevation = CardConfig.cardElevation,
focusedElevation = CardConfig.cardElevation,
hoveredElevation = CardConfig.cardElevation,
draggedElevation = CardConfig.cardElevation,
disabledElevation = CardConfig.cardElevation
)
/** /**
* 根据背景颜色、主题模式和用户设置确定内容颜色 * 根据背景颜色、主题模式和用户设置确定内容颜色
*/ */
@@ -104,4 +120,4 @@ private fun determineContentColor(originalColor: Color): Color {
isDarkTheme -> Color.White isDarkTheme -> Color.White
else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White
} }
} }

View File

@@ -3,260 +3,602 @@ package com.sukisu.ultra.ui.theme
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
sealed class ThemeColors { sealed class ThemeColors {
abstract val Primary: Color // 浅色
abstract val Secondary: Color abstract val primaryLight: Color
abstract val Tertiary: Color abstract val onPrimaryLight: Color
abstract val OnPrimary: Color abstract val primaryContainerLight: Color
abstract val OnSecondary: Color abstract val onPrimaryContainerLight: Color
abstract val OnTertiary: Color abstract val secondaryLight: Color
abstract val PrimaryContainer: Color abstract val onSecondaryLight: Color
abstract val SecondaryContainer: Color abstract val secondaryContainerLight: Color
abstract val TertiaryContainer: Color abstract val onSecondaryContainerLight: Color
abstract val OnPrimaryContainer: Color abstract val tertiaryLight: Color
abstract val OnSecondaryContainer: Color abstract val onTertiaryLight: Color
abstract val OnTertiaryContainer: Color abstract val tertiaryContainerLight: Color
abstract val ButtonContrast: Color abstract val onTertiaryContainerLight: Color
abstract val errorLight: Color
// 表面颜色 abstract val onErrorLight: Color
abstract val Surface: Color abstract val errorContainerLight: Color
abstract val SurfaceVariant: Color abstract val onErrorContainerLight: Color
abstract val OnSurface: Color abstract val backgroundLight: Color
abstract val OnSurfaceVariant: Color abstract val onBackgroundLight: Color
abstract val surfaceLight: Color
// 错误状态颜色 abstract val onSurfaceLight: Color
abstract val Error: Color abstract val surfaceVariantLight: Color
abstract val OnError: Color abstract val onSurfaceVariantLight: Color
abstract val ErrorContainer: Color abstract val outlineLight: Color
abstract val OnErrorContainer: Color abstract val outlineVariantLight: Color
abstract val scrimLight: Color
// 边框和背景色 abstract val inverseSurfaceLight: Color
abstract val Outline: Color abstract val inverseOnSurfaceLight: Color
abstract val OutlineVariant: Color abstract val inversePrimaryLight: Color
abstract val Background: Color abstract val surfaceDimLight: Color
abstract val OnBackground: Color abstract val surfaceBrightLight: Color
abstract val surfaceContainerLowestLight: Color
abstract val surfaceContainerLowLight: Color
abstract val surfaceContainerLight: Color
abstract val surfaceContainerHighLight: Color
abstract val surfaceContainerHighestLight: Color
// 深色
abstract val primaryDark: Color
abstract val onPrimaryDark: Color
abstract val primaryContainerDark: Color
abstract val onPrimaryContainerDark: Color
abstract val secondaryDark: Color
abstract val onSecondaryDark: Color
abstract val secondaryContainerDark: Color
abstract val onSecondaryContainerDark: Color
abstract val tertiaryDark: Color
abstract val onTertiaryDark: Color
abstract val tertiaryContainerDark: Color
abstract val onTertiaryContainerDark: Color
abstract val errorDark: Color
abstract val onErrorDark: Color
abstract val errorContainerDark: Color
abstract val onErrorContainerDark: Color
abstract val backgroundDark: Color
abstract val onBackgroundDark: Color
abstract val surfaceDark: Color
abstract val onSurfaceDark: Color
abstract val surfaceVariantDark: Color
abstract val onSurfaceVariantDark: Color
abstract val outlineDark: Color
abstract val outlineVariantDark: Color
abstract val scrimDark: Color
abstract val inverseSurfaceDark: Color
abstract val inverseOnSurfaceDark: Color
abstract val inversePrimaryDark: Color
abstract val surfaceDimDark: Color
abstract val surfaceBrightDark: Color
abstract val surfaceContainerLowestDark: Color
abstract val surfaceContainerLowDark: Color
abstract val surfaceContainerDark: Color
abstract val surfaceContainerHighDark: Color
abstract val surfaceContainerHighestDark: Color
// 默认主题 (蓝色) // 默认主题 (蓝色)
object Default : ThemeColors() { object Default : ThemeColors() {
override val Primary = Color(0xFF2196F3) override val primaryLight = Color(0xFF415F91)
override val Secondary = Color(0xFF64B5F6) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFF0D47A1) override val primaryContainerLight = Color(0xFFD6E3FF)
override val OnPrimary = Color(0xFFFFFFFF) override val onPrimaryContainerLight = Color(0xFF284777)
override val OnSecondary = Color(0xFFFFFFFF) override val secondaryLight = Color(0xFF565F71)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFD6EAFF) override val secondaryContainerLight = Color(0xFFDAE2F9)
override val SecondaryContainer = Color(0xFFE3F2FD) override val onSecondaryContainerLight = Color(0xFF3E4759)
override val TertiaryContainer = Color(0xFFCFD8DC) override val tertiaryLight = Color(0xFF705575)
override val OnPrimaryContainer = Color(0xFF0A3049) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF0D3C61) override val tertiaryContainerLight = Color(0xFFFAD8FD)
override val OnTertiaryContainer = Color(0xFF071D41) override val onTertiaryContainerLight = Color(0xFF573E5C)
override val ButtonContrast = Color(0xFF2196F3) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFF9F9FF)
override val onBackgroundLight = Color(0xFF191C20)
override val surfaceLight = Color(0xFFF9F9FF)
override val onSurfaceLight = Color(0xFF191C20)
override val surfaceVariantLight = Color(0xFFE0E2EC)
override val onSurfaceVariantLight = Color(0xFF44474E)
override val outlineLight = Color(0xFF74777F)
override val outlineVariantLight = Color(0xFFC4C6D0)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF2E3036)
override val inverseOnSurfaceLight = Color(0xFFF0F0F7)
override val inversePrimaryLight = Color(0xFFAAC7FF)
override val surfaceDimLight = Color(0xFFD9D9E0)
override val surfaceBrightLight = Color(0xFFF9F9FF)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFF3F3FA)
override val surfaceContainerLight = Color(0xFFEDEDF4)
override val surfaceContainerHighLight = Color(0xFFE7E8EE)
override val surfaceContainerHighestLight = Color(0xFFE2E2E9)
override val Surface = Color(0xFFF5F9FF) override val primaryDark = Color(0xFFAAC7FF)
override val SurfaceVariant = Color(0xFFEDF5FE) override val onPrimaryDark = Color(0xFF0A305F)
override val OnSurface = Color(0xFF1A1C1E) override val primaryContainerDark = Color(0xFF284777)
override val OnSurfaceVariant = Color(0xFF42474E) override val onPrimaryContainerDark = Color(0xFFD6E3FF)
override val secondaryDark = Color(0xFFBEC6DC)
override val Error = Color(0xFFB00020) override val onSecondaryDark = Color(0xFF283141)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF3E4759)
override val ErrorContainer = Color(0xFFFDE7E9) override val onSecondaryContainerDark = Color(0xFFDAE2F9)
override val OnErrorContainer = Color(0xFF410008) override val tertiaryDark = Color(0xFFDDBCE0)
override val onTertiaryDark = Color(0xFF3F2844)
override val Outline = Color(0xFFBAC3CF) override val tertiaryContainerDark = Color(0xFF573E5C)
override val OutlineVariant = Color(0xFFDFE3EB) override val onTertiaryContainerDark = Color(0xFFFAD8FD)
override val Background = Color(0xFFFAFCFF) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF1A1C1E) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF111318)
override val onBackgroundDark = Color(0xFFE2E2E9)
override val surfaceDark = Color(0xFF111318)
override val onSurfaceDark = Color(0xFFE2E2E9)
override val surfaceVariantDark = Color(0xFF44474E)
override val onSurfaceVariantDark = Color(0xFFC4C6D0)
override val outlineDark = Color(0xFF8E9099)
override val outlineVariantDark = Color(0xFF44474E)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFE2E2E9)
override val inverseOnSurfaceDark = Color(0xFF2E3036)
override val inversePrimaryDark = Color(0xFF415F91)
override val surfaceDimDark = Color(0xFF111318)
override val surfaceBrightDark = Color(0xFF37393E)
override val surfaceContainerLowestDark = Color(0xFF0C0E13)
override val surfaceContainerLowDark = Color(0xFF191C20)
override val surfaceContainerDark = Color(0xFF1D2024)
override val surfaceContainerHighDark = Color(0xFF282A2F)
override val surfaceContainerHighestDark = Color(0xFF33353A)
} }
// 绿色主题 // 绿色主题
object Green : ThemeColors() { object Green : ThemeColors() {
override val Primary = Color(0xFF43A047) override val primaryLight = Color(0xFF4C662B)
override val Secondary = Color(0xFF66BB6A) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFF1B5E20) override val primaryContainerLight = Color(0xFFCDEDA3)
override val OnPrimary = Color(0xFFFFFFFF) override val onPrimaryContainerLight = Color(0xFF354E16)
override val OnSecondary = Color(0xFFFFFFFF) override val secondaryLight = Color(0xFF586249)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFD8EFDB) override val secondaryContainerLight = Color(0xFFDCE7C8)
override val SecondaryContainer = Color(0xFFE8F5E9) override val onSecondaryContainerLight = Color(0xFF404A33)
override val TertiaryContainer = Color(0xFFB9F6CA) override val tertiaryLight = Color(0xFF386663)
override val OnPrimaryContainer = Color(0xFF0A280D) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF0E2912) override val tertiaryContainerLight = Color(0xFFBCECE7)
override val OnTertiaryContainer = Color(0xFF051B07) override val onTertiaryContainerLight = Color(0xFF1F4E4B)
override val ButtonContrast = Color(0xFF43A047) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFF9FAEF)
override val onBackgroundLight = Color(0xFF1A1C16)
override val surfaceLight = Color(0xFFF9FAEF)
override val onSurfaceLight = Color(0xFF1A1C16)
override val surfaceVariantLight = Color(0xFFE1E4D5)
override val onSurfaceVariantLight = Color(0xFF44483D)
override val outlineLight = Color(0xFF75796C)
override val outlineVariantLight = Color(0xFFC5C8BA)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF2F312A)
override val inverseOnSurfaceLight = Color(0xFFF1F2E6)
override val inversePrimaryLight = Color(0xFFB1D18A)
override val surfaceDimLight = Color(0xFFDADBD0)
override val surfaceBrightLight = Color(0xFFF9FAEF)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFF3F4E9)
override val surfaceContainerLight = Color(0xFFEEEFE3)
override val surfaceContainerHighLight = Color(0xFFE8E9DE)
override val surfaceContainerHighestLight = Color(0xFFE2E3D8)
override val Surface = Color(0xFFF6FBF6) override val primaryDark = Color(0xFFB1D18A)
override val SurfaceVariant = Color(0xFFEDF7EE) override val onPrimaryDark = Color(0xFF1F3701)
override val OnSurface = Color(0xFF191C19) override val primaryContainerDark = Color(0xFF354E16)
override val OnSurfaceVariant = Color(0xFF414941) override val onPrimaryContainerDark = Color(0xFFCDEDA3)
override val secondaryDark = Color(0xFFBFCBAD)
override val Error = Color(0xFFC62828) override val onSecondaryDark = Color(0xFF2A331E)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF404A33)
override val ErrorContainer = Color(0xFFF8D7DA) override val onSecondaryContainerDark = Color(0xFFDCE7C8)
override val OnErrorContainer = Color(0xFF4A0808) override val tertiaryDark = Color(0xFFA0D0CB)
override val onTertiaryDark = Color(0xFF003735)
override val Outline = Color(0xFFBDC9BF) override val tertiaryContainerDark = Color(0xFF1F4E4B)
override val OutlineVariant = Color(0xFFDDE6DE) override val onTertiaryContainerDark = Color(0xFFBCECE7)
override val Background = Color(0xFFFBFDFB) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF191C19) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF12140E)
override val onBackgroundDark = Color(0xFFE2E3D8)
override val surfaceDark = Color(0xFF12140E)
override val onSurfaceDark = Color(0xFFE2E3D8)
override val surfaceVariantDark = Color(0xFF44483D)
override val onSurfaceVariantDark = Color(0xFFC5C8BA)
override val outlineDark = Color(0xFF8F9285)
override val outlineVariantDark = Color(0xFF44483D)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFE2E3D8)
override val inverseOnSurfaceDark = Color(0xFF2F312A)
override val inversePrimaryDark = Color(0xFF4C662B)
override val surfaceDimDark = Color(0xFF12140E)
override val surfaceBrightDark = Color(0xFF383A32)
override val surfaceContainerLowestDark = Color(0xFF0C0F09)
override val surfaceContainerLowDark = Color(0xFF1A1C16)
override val surfaceContainerDark = Color(0xFF1E201A)
override val surfaceContainerHighDark = Color(0xFF282B24)
override val surfaceContainerHighestDark = Color(0xFF33362E)
} }
// 紫色主题 // 紫色主题
object Purple : ThemeColors() { object Purple : ThemeColors() {
override val Primary = Color(0xFF9C27B0) override val primaryLight = Color(0xFF7C4E7E)
override val Secondary = Color(0xFFBA68C8) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFF6A1B9A) override val primaryContainerLight = Color(0xFFFFD6FC)
override val OnPrimary = Color(0xFFFFFFFF) override val onPrimaryContainerLight = Color(0xFF623765)
override val OnSecondary = Color(0xFFFFFFFF) override val secondaryLight = Color(0xFF6C586B)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFF3D8F8) override val secondaryContainerLight = Color(0xFFF5DBF1)
override val SecondaryContainer = Color(0xFFF5E9F7) override val onSecondaryContainerLight = Color(0xFF534152)
override val TertiaryContainer = Color(0xFFE1BEE7) override val tertiaryLight = Color(0xFF825249)
override val OnPrimaryContainer = Color(0xFF2A0934) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF3C0F50) override val tertiaryContainerLight = Color(0xFFFFDAD4)
override val OnTertiaryContainer = Color(0xFF1D0830) override val onTertiaryContainerLight = Color(0xFF673B33)
override val ButtonContrast = Color(0xFF9C27B0) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFFFF7FA)
override val onBackgroundLight = Color(0xFF1F1A1F)
override val surfaceLight = Color(0xFFFFF7FA)
override val onSurfaceLight = Color(0xFF1F1A1F)
override val surfaceVariantLight = Color(0xFFEDDFE8)
override val onSurfaceVariantLight = Color(0xFF4D444C)
override val outlineLight = Color(0xFF7F747C)
override val outlineVariantLight = Color(0xFFD0C3CC)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF352F34)
override val inverseOnSurfaceLight = Color(0xFFF9EEF4)
override val inversePrimaryLight = Color(0xFFECB4EC)
override val surfaceDimLight = Color(0xFFE2D7DE)
override val surfaceBrightLight = Color(0xFFFFF7FA)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFFCF0F7)
override val surfaceContainerLight = Color(0xFFF6EBF2)
override val surfaceContainerHighLight = Color(0xFFF0E5EC)
override val surfaceContainerHighestLight = Color(0xFFEBDFE6)
override val Surface = Color(0xFFFCF6FF) override val primaryDark = Color(0xFFECB4EC)
override val SurfaceVariant = Color(0xFFF5EEFA) override val onPrimaryDark = Color(0xFF49204D)
override val OnSurface = Color(0xFF1D1B1E) override val primaryContainerDark = Color(0xFF623765)
override val OnSurfaceVariant = Color(0xFF49454E) override val onPrimaryContainerDark = Color(0xFFFFD6FC)
override val secondaryDark = Color(0xFFD8BFD5)
override val Error = Color(0xFFD50000) override val onSecondaryDark = Color(0xFF3B2B3B)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF534152)
override val ErrorContainer = Color(0xFFFFDCD5) override val onSecondaryContainerDark = Color(0xFFF5DBF1)
override val OnErrorContainer = Color(0xFF480000) override val tertiaryDark = Color(0xFFF6B8AD)
override val onTertiaryDark = Color(0xFF4C251F)
override val Outline = Color(0xFFC9B9D0) override val tertiaryContainerDark = Color(0xFF673B33)
override val OutlineVariant = Color(0xFFE8DAED) override val onTertiaryContainerDark = Color(0xFFFFDAD4)
override val Background = Color(0xFFFFFBFF) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF1D1B1E) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF171216)
override val onBackgroundDark = Color(0xFFEBDFE6)
override val surfaceDark = Color(0xFF171216)
override val onSurfaceDark = Color(0xFFEBDFE6)
override val surfaceVariantDark = Color(0xFF4D444C)
override val onSurfaceVariantDark = Color(0xFFD0C3CC)
override val outlineDark = Color(0xFF998D96)
override val outlineVariantDark = Color(0xFF4D444C)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFEBDFE6)
override val inverseOnSurfaceDark = Color(0xFF352F34)
override val inversePrimaryDark = Color(0xFF7C4E7E)
override val surfaceDimDark = Color(0xFF171216)
override val surfaceBrightDark = Color(0xFF3E373D)
override val surfaceContainerLowestDark = Color(0xFF110D11)
override val surfaceContainerLowDark = Color(0xFF1F1A1F)
override val surfaceContainerDark = Color(0xFF231E23)
override val surfaceContainerHighDark = Color(0xFF2E282D)
override val surfaceContainerHighestDark = Color(0xFF393338)
} }
// 橙色主题 // 橙色主题
object Orange : ThemeColors() { object Orange : ThemeColors() {
override val Primary = Color(0xFFFF9800) override val primaryLight = Color(0xFF8B4F24)
override val Secondary = Color(0xFFFFB74D) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFFE65100) override val primaryContainerLight = Color(0xFFFFDCC7)
override val OnPrimary = Color(0xFFFFFFFF) override val onPrimaryContainerLight = Color(0xFF6E390E)
override val OnSecondary = Color(0xFF000000) override val secondaryLight = Color(0xFF755846)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFECCC) override val secondaryContainerLight = Color(0xFFFFDCC7)
override val SecondaryContainer = Color(0xFFFFF0D9) override val onSecondaryContainerLight = Color(0xFF5B4130)
override val TertiaryContainer = Color(0xFFFFD180) override val tertiaryLight = Color(0xFF865219)
override val OnPrimaryContainer = Color(0xFF351F00) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF3D2800) override val tertiaryContainerLight = Color(0xFFFFDCBF)
override val OnTertiaryContainer = Color(0xFF2E1500) override val onTertiaryContainerLight = Color(0xFF6A3B01)
override val ButtonContrast = Color(0xFFFF9800) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFFFF8F5)
override val onBackgroundLight = Color(0xFF221A15)
override val surfaceLight = Color(0xFFFFF8F5)
override val onSurfaceLight = Color(0xFF221A15)
override val surfaceVariantLight = Color(0xFFF4DED3)
override val onSurfaceVariantLight = Color(0xFF52443C)
override val outlineLight = Color(0xFF84746A)
override val outlineVariantLight = Color(0xFFD7C3B8)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF382E29)
override val inverseOnSurfaceLight = Color(0xFFFFEDE5)
override val inversePrimaryLight = Color(0xFFFFB787)
override val surfaceDimLight = Color(0xFFE7D7CE)
override val surfaceBrightLight = Color(0xFFFFF8F5)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFFFF1EA)
override val surfaceContainerLight = Color(0xFFFCEBE2)
override val surfaceContainerHighLight = Color(0xFFF6E5DC)
override val surfaceContainerHighestLight = Color(0xFFF0DFD7)
override val Surface = Color(0xFFFFF8F3) override val primaryDark = Color(0xFFFFB787)
override val SurfaceVariant = Color(0xFFFFF0E6) override val onPrimaryDark = Color(0xFF502400)
override val OnSurface = Color(0xFF1F1B16) override val primaryContainerDark = Color(0xFF6E390E)
override val OnSurfaceVariant = Color(0xFF4E4639) override val onPrimaryContainerDark = Color(0xFFFFDCC7)
override val secondaryDark = Color(0xFFE5BFA8)
override val Error = Color(0xFFD32F2F) override val onSecondaryDark = Color(0xFF422B1B)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF5B4130)
override val ErrorContainer = Color(0xFFFFDBC8) override val onSecondaryContainerDark = Color(0xFFFFDCC7)
override val OnErrorContainer = Color(0xFF490700) override val tertiaryDark = Color(0xFFFDB876)
override val onTertiaryDark = Color(0xFF4B2800)
override val Outline = Color(0xFFD6C3AD) override val tertiaryContainerDark = Color(0xFF6A3B01)
override val OutlineVariant = Color(0xFFEFDFCC) override val onTertiaryContainerDark = Color(0xFFFFDCBF)
override val Background = Color(0xFFFFFBFF) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF1F1B16) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF19120D)
override val onBackgroundDark = Color(0xFFF0DFD7)
override val surfaceDark = Color(0xFF19120D)
override val onSurfaceDark = Color(0xFFF0DFD7)
override val surfaceVariantDark = Color(0xFF52443C)
override val onSurfaceVariantDark = Color(0xFFD7C3B8)
override val outlineDark = Color(0xFF9F8D83)
override val outlineVariantDark = Color(0xFF52443C)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFF0DFD7)
override val inverseOnSurfaceDark = Color(0xFF382E29)
override val inversePrimaryDark = Color(0xFF8B4F24)
override val surfaceDimDark = Color(0xFF19120D)
override val surfaceBrightDark = Color(0xFF413731)
override val surfaceContainerLowestDark = Color(0xFF140D08)
override val surfaceContainerLowDark = Color(0xFF221A15)
override val surfaceContainerDark = Color(0xFF261E19)
override val surfaceContainerHighDark = Color(0xFF312823)
override val surfaceContainerHighestDark = Color(0xFF3D332D)
} }
// 粉色主题 // 粉色主题
object Pink : ThemeColors() { object Pink : ThemeColors() {
override val Primary = Color(0xFFE91E63) override val primaryLight = Color(0xFF8C4A60)
override val Secondary = Color(0xFFF06292) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFF880E4F) override val primaryContainerLight = Color(0xFFFFD9E2)
override val OnPrimary = Color(0xFFFFFFFF) override val onPrimaryContainerLight = Color(0xFF703348)
override val OnSecondary = Color(0xFFFFFFFF) override val secondaryLight = Color(0xFF8B4A62)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFCE4EC) override val secondaryContainerLight = Color(0xFFFFD9E3)
override val SecondaryContainer = Color(0xFFFCE4EC) override val onSecondaryContainerLight = Color(0xFF6F334B)
override val TertiaryContainer = Color(0xFFF8BBD0) override val tertiaryLight = Color(0xFF8B4A62)
override val OnPrimaryContainer = Color(0xFF3B0819) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF3B0819) override val tertiaryContainerLight = Color(0xFFFFD9E3)
override val OnTertiaryContainer = Color(0xFF2B0516) override val onTertiaryContainerLight = Color(0xFF6F334B)
override val ButtonContrast = Color(0xFFE91E63) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFFFF8F8)
override val onBackgroundLight = Color(0xFF22191B)
override val surfaceLight = Color(0xFFFFF8F8)
override val onSurfaceLight = Color(0xFF22191B)
override val surfaceVariantLight = Color(0xFFF2DDE1)
override val onSurfaceVariantLight = Color(0xFF514346)
override val outlineLight = Color(0xFF837377)
override val outlineVariantLight = Color(0xFFD5C2C5)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF372E30)
override val inverseOnSurfaceLight = Color(0xFFFDEDEF)
override val inversePrimaryLight = Color(0xFFFFB1C7)
override val surfaceDimLight = Color(0xFFE6D6D9)
override val surfaceBrightLight = Color(0xFFFFF8F8)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFFFF0F2)
override val surfaceContainerLight = Color(0xFFFBEAED)
override val surfaceContainerHighLight = Color(0xFFF5E4E7)
override val surfaceContainerHighestLight = Color(0xFFEFDFE1)
override val Surface = Color(0xFFFFF7F9) override val primaryDark = Color(0xFFFFB1C7)
override val SurfaceVariant = Color(0xFFFCEEF2) override val onPrimaryDark = Color(0xFF541D32)
override val OnSurface = Color(0xFF201A1C) override val primaryContainerDark = Color(0xFF703348)
override val OnSurfaceVariant = Color(0xFF534347) override val onPrimaryContainerDark = Color(0xFFFFD9E2)
override val secondaryDark = Color(0xFFFFB0CB)
override val Error = Color(0xFFB71C1C) override val onSecondaryDark = Color(0xFF541D34)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF6F334B)
override val ErrorContainer = Color(0xFFFFDAD6) override val onSecondaryContainerDark = Color(0xFFFFD9E3)
override val OnErrorContainer = Color(0xFF410002) override val tertiaryDark = Color(0xFFFFB0CB)
override val onTertiaryDark = Color(0xFF541D34)
override val Outline = Color(0xFFD6BABF) override val tertiaryContainerDark = Color(0xFF6F334B)
override val OutlineVariant = Color(0xFFEFDDE0) override val onTertiaryContainerDark = Color(0xFFFFD9E3)
override val Background = Color(0xFFFFFBFF) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF201A1C) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF191113)
override val onBackgroundDark = Color(0xFFEFDFE1)
override val surfaceDark = Color(0xFF191113)
override val onSurfaceDark = Color(0xFFEFDFE1)
override val surfaceVariantDark = Color(0xFF514346)
override val onSurfaceVariantDark = Color(0xFFD5C2C5)
override val outlineDark = Color(0xFF9E8C90)
override val outlineVariantDark = Color(0xFF514346)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFEFDFE1)
override val inverseOnSurfaceDark = Color(0xFF372E30)
override val inversePrimaryDark = Color(0xFF8C4A60)
override val surfaceDimDark = Color(0xFF191113)
override val surfaceBrightDark = Color(0xFF413739)
override val surfaceContainerLowestDark = Color(0xFF140C0E)
override val surfaceContainerLowDark = Color(0xFF22191B)
override val surfaceContainerDark = Color(0xFF261D1F)
override val surfaceContainerHighDark = Color(0xFF31282A)
override val surfaceContainerHighestDark = Color(0xFF3C3234)
} }
// 灰色主题 // 灰色主题
object Gray : ThemeColors() { object Gray : ThemeColors() {
override val Primary = Color(0xFF607D8B) override val primaryLight = Color(0xFF5B5C5C)
override val Secondary = Color(0xFF90A4AE) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFF455A64) override val primaryContainerLight = Color(0xFF747474)
override val OnPrimary = Color(0xFFFFFFFF) override val onPrimaryContainerLight = Color(0xFFFEFCFC)
override val OnSecondary = Color(0xFFFFFFFF) override val secondaryLight = Color(0xFF5F5E5E)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFECEFF1) override val secondaryContainerLight = Color(0xFFE4E2E1)
override val SecondaryContainer = Color(0xFFECEFF1) override val onSecondaryContainerLight = Color(0xFF656464)
override val TertiaryContainer = Color(0xFFCFD8DC) override val tertiaryLight = Color(0xFF5E5B5D)
override val OnPrimaryContainer = Color(0xFF1A2327) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF1A2327) override val tertiaryContainerLight = Color(0xFF777375)
override val OnTertiaryContainer = Color(0xFF121A1D) override val onTertiaryContainerLight = Color(0xFFFFFBFF)
override val ButtonContrast = Color(0xFF607D8B) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFFCF8F8)
override val onBackgroundLight = Color(0xFF1C1B1B)
override val surfaceLight = Color(0xFFFCF8F8)
override val onSurfaceLight = Color(0xFF1C1B1B)
override val surfaceVariantLight = Color(0xFFE0E3E3)
override val onSurfaceVariantLight = Color(0xFF444748)
override val outlineLight = Color(0xFF747878)
override val outlineVariantLight = Color(0xFFC4C7C7)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF313030)
override val inverseOnSurfaceLight = Color(0xFFF4F0EF)
override val inversePrimaryLight = Color(0xFFC7C6C6)
override val surfaceDimLight = Color(0xFFDDD9D8)
override val surfaceBrightLight = Color(0xFFFCF8F8)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFF7F3F2)
override val surfaceContainerLight = Color(0xFFF1EDEC)
override val surfaceContainerHighLight = Color(0xFFEBE7E7)
override val surfaceContainerHighestLight = Color(0xFFE5E2E1)
override val Surface = Color(0xFFF6F9FB) override val primaryDark = Color(0xFFC7C6C6)
override val SurfaceVariant = Color(0xFFEEF2F4) override val onPrimaryDark = Color(0xFF303031)
override val OnSurface = Color(0xFF191C1E) override val primaryContainerDark = Color(0xFF919190)
override val OnSurfaceVariant = Color(0xFF41484D) override val onPrimaryContainerDark = Color(0xFF161718)
override val secondaryDark = Color(0xFFC8C6C5)
override val Error = Color(0xFFC62828) override val onSecondaryDark = Color(0xFF303030)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF474746)
override val ErrorContainer = Color(0xFFFFDAD6) override val onSecondaryContainerDark = Color(0xFFB7B5B4)
override val OnErrorContainer = Color(0xFF410002) override val tertiaryDark = Color(0xFFCAC5C7)
override val onTertiaryDark = Color(0xFF323031)
override val Outline = Color(0xFFBDC1C4) override val tertiaryContainerDark = Color(0xFF948F91)
override val OutlineVariant = Color(0xFFDDE1E3) override val onTertiaryContainerDark = Color(0xFF181718)
override val Background = Color(0xFFFBFCFE) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF191C1E) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF141313)
override val onBackgroundDark = Color(0xFFE5E2E1)
override val surfaceDark = Color(0xFF141313)
override val onSurfaceDark = Color(0xFFE5E2E1)
override val surfaceVariantDark = Color(0xFF444748)
override val onSurfaceVariantDark = Color(0xFFC4C7C7)
override val outlineDark = Color(0xFF8E9192)
override val outlineVariantDark = Color(0xFF444748)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFE5E2E1)
override val inverseOnSurfaceDark = Color(0xFF313030)
override val inversePrimaryDark = Color(0xFF5E5E5E)
override val surfaceDimDark = Color(0xFF141313)
override val surfaceBrightDark = Color(0xFF3A3939)
override val surfaceContainerLowestDark = Color(0xFF0E0E0E)
override val surfaceContainerLowDark = Color(0xFF1C1B1B)
override val surfaceContainerDark = Color(0xFF201F1F)
override val surfaceContainerHighDark = Color(0xFF2A2A2A)
override val surfaceContainerHighestDark = Color(0xFF353434)
} }
// 黄色主题 // 黄色主题
object Yellow : ThemeColors() { object Yellow : ThemeColors() {
override val Primary = Color(0xFFFFC107) override val primaryLight = Color(0xFF6D5E0F)
override val Secondary = Color(0xFFFFD54F) override val onPrimaryLight = Color(0xFFFFFFFF)
override val Tertiary = Color(0xFFFF8F00) override val primaryContainerLight = Color(0xFFF8E288)
override val OnPrimary = Color(0xFF000000) override val onPrimaryContainerLight = Color(0xFF534600)
override val OnSecondary = Color(0xFF000000) override val secondaryLight = Color(0xFF6D5E0F)
override val OnTertiary = Color(0xFFFFFFFF) override val onSecondaryLight = Color(0xFFFFFFFF)
override val PrimaryContainer = Color(0xFFFFF8E1) override val secondaryContainerLight = Color(0xFFF7E388)
override val SecondaryContainer = Color(0xFFFFF8E1) override val onSecondaryContainerLight = Color(0xFF534600)
override val TertiaryContainer = Color(0xFFFFECB3) override val tertiaryLight = Color(0xFF685F13)
override val OnPrimaryContainer = Color(0xFF332A00) override val onTertiaryLight = Color(0xFFFFFFFF)
override val OnSecondaryContainer = Color(0xFF332A00) override val tertiaryContainerLight = Color(0xFFF1E58A)
override val OnTertiaryContainer = Color(0xFF221200) override val onTertiaryContainerLight = Color(0xFF4F4800)
override val ButtonContrast = Color(0xFFFFC107) override val errorLight = Color(0xFFBA1A1A)
override val onErrorLight = Color(0xFFFFFFFF)
override val errorContainerLight = Color(0xFFFFDAD6)
override val onErrorContainerLight = Color(0xFF93000A)
override val backgroundLight = Color(0xFFFFF9ED)
override val onBackgroundLight = Color(0xFF1E1C13)
override val surfaceLight = Color(0xFFFFF9ED)
override val onSurfaceLight = Color(0xFF1E1C13)
override val surfaceVariantLight = Color(0xFFE9E2D0)
override val onSurfaceVariantLight = Color(0xFF4B4739)
override val outlineLight = Color(0xFF7C7768)
override val outlineVariantLight = Color(0xFFCDC6B4)
override val scrimLight = Color(0xFF000000)
override val inverseSurfaceLight = Color(0xFF333027)
override val inverseOnSurfaceLight = Color(0xFFF7F0E2)
override val inversePrimaryLight = Color(0xFFDAC66F)
override val surfaceDimLight = Color(0xFFE0D9CC)
override val surfaceBrightLight = Color(0xFFFFF9ED)
override val surfaceContainerLowestLight = Color(0xFFFFFFFF)
override val surfaceContainerLowLight = Color(0xFFFAF3E5)
override val surfaceContainerLight = Color(0xFFF4EDDF)
override val surfaceContainerHighLight = Color(0xFFEEE8DA)
override val surfaceContainerHighestLight = Color(0xFFE8E2D4)
override val Surface = Color(0xFFFFFAF3) override val primaryDark = Color(0xFFDAC66F)
override val SurfaceVariant = Color(0xFFFFF7E6) override val onPrimaryDark = Color(0xFF393000)
override val OnSurface = Color(0xFF1F1C17) override val primaryContainerDark = Color(0xFF534600)
override val OnSurfaceVariant = Color(0xFF4E4A3C) override val onPrimaryContainerDark = Color(0xFFF8E288)
override val secondaryDark = Color(0xFFDAC76F)
override val Error = Color(0xFFB71C1C) override val onSecondaryDark = Color(0xFF393000)
override val OnError = Color(0xFFFFFFFF) override val secondaryContainerDark = Color(0xFF534600)
override val ErrorContainer = Color(0xFFFFDAD6) override val onSecondaryContainerDark = Color(0xFFF7E388)
override val OnErrorContainer = Color(0xFF410002) override val tertiaryDark = Color(0xFFD4C871)
override val onTertiaryDark = Color(0xFF363100)
override val Outline = Color(0xFFD1C8AF) override val tertiaryContainerDark = Color(0xFF4F4800)
override val OutlineVariant = Color(0xFFEEE8D7) override val onTertiaryContainerDark = Color(0xFFF1E58A)
override val Background = Color(0xFFFFFCF8) override val errorDark = Color(0xFFFFB4AB)
override val OnBackground = Color(0xFF1F1C17) override val onErrorDark = Color(0xFF690005)
override val errorContainerDark = Color(0xFF93000A)
override val onErrorContainerDark = Color(0xFFFFDAD6)
override val backgroundDark = Color(0xFF15130B)
override val onBackgroundDark = Color(0xFFE8E2D4)
override val surfaceDark = Color(0xFF15130B)
override val onSurfaceDark = Color(0xFFE8E2D4)
override val surfaceVariantDark = Color(0xFF4B4739)
override val onSurfaceVariantDark = Color(0xFFCDC6B4)
override val outlineDark = Color(0xFF969080)
override val outlineVariantDark = Color(0xFF4B4739)
override val scrimDark = Color(0xFF000000)
override val inverseSurfaceDark = Color(0xFFE8E2D4)
override val inverseOnSurfaceDark = Color(0xFF333027)
override val inversePrimaryDark = Color(0xFF6D5E0F)
override val surfaceDimDark = Color(0xFF15130B)
override val surfaceBrightDark = Color(0xFF3C3930)
override val surfaceContainerLowestDark = Color(0xFF100E07)
override val surfaceContainerLowDark = Color(0xFF1E1C13)
override val surfaceContainerDark = Color(0xFF222017)
override val surfaceContainerHighDark = Color(0xFF2C2A21)
override val surfaceContainerHighestDark = Color(0xFF37352B)
} }
companion object { companion object {

View File

@@ -47,6 +47,7 @@ import com.sukisu.ultra.ui.util.saveTransformedBackground
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
@@ -110,6 +111,9 @@ fun KernelSUTheme(
if (!isCustomAlphaSet) { if (!isCustomAlphaSet) {
cardAlpha = if (systemIsDark) 0.50f else 1f cardAlpha = if (systemIsDark) 0.50f else 1f
} }
if (!isCustomDimSet) {
cardDim = if (systemIsDark) 0.5f else 0f
}
save(context) save(context)
} }
} }
@@ -147,7 +151,9 @@ fun KernelSUTheme(
// 根据暗色模式和自定义背景调整卡片配置 // 根据暗色模式和自定义背景调整卡片配置
val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null
if (darkTheme && !dynamicColor) { if (darkTheme && !dynamicColor) {
CardConfig.setDarkModeDefaults() CardConfig.setThemeDefaults(true)
} else if (!darkTheme && !dynamicColor) {
CardConfig.setThemeDefaults(false)
} }
CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground) CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground)
@@ -199,6 +205,9 @@ fun KernelSUTheme(
} }
} }
// 计算适用的暗化值
val dimFactor = CardConfig.cardDim
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography typography = Typography
@@ -208,7 +217,8 @@ fun KernelSUTheme(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.zIndex(-2f) .zIndex(-2f)
.background(if (darkTheme) Color.Black else Color.White) .background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }
else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background })
) )
// 自定义背景层 // 自定义背景层
@@ -234,13 +244,13 @@ fun KernelSUTheme(
) )
} }
// 亮度调节层 // 亮度调节层 (根据cardDim调整)
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background( .background(
if (darkTheme) Color.Black.copy(alpha = 0.6f) if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f)
else Color.White.copy(alpha = 0.1f) else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f)
) )
) )
@@ -252,8 +262,8 @@ fun KernelSUTheme(
Brush.radialGradient( Brush.radialGradient(
colors = listOf( colors = listOf(
Color.Transparent, Color.Transparent,
if (darkTheme) Color.Black.copy(alpha = 0.5f) if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f)
else Color.Black.copy(alpha = 0.2f) else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f)
), ),
radius = 1200f radius = 1200f
) )
@@ -279,54 +289,73 @@ fun KernelSUTheme(
*/ */
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
private fun createDynamicDarkColorScheme(context: Context) = private fun createDynamicDarkColorScheme(context: Context): ColorScheme {
dynamicDarkColorScheme(context).copy( val scheme = dynamicDarkColorScheme(context)
background = Color.Transparent, return scheme.copy(
surface = Color.Transparent, background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
onBackground = Color.White, surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
onSurface = Color.White onBackground = scheme.onBackground,
onSurface = scheme.onSurface
) )
}
/** /**
* 创建动态浅色颜色方案 * 创建动态浅色颜色方案
*/ */
@RequiresApi(Build.VERSION_CODES.S) @RequiresApi(Build.VERSION_CODES.S)
@Composable @Composable
private fun createDynamicLightColorScheme(context: Context) = private fun createDynamicLightColorScheme(context: Context): ColorScheme {
dynamicLightColorScheme(context).copy( val scheme = dynamicLightColorScheme(context)
background = Color.Transparent, return scheme.copy(
surface = Color.Transparent background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.background,
surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else scheme.surface,
onBackground = scheme.onBackground,
onSurface = scheme.onSurface
) )
}
/** /**
* 创建深色颜色方案 * 创建深色颜色方案
*/ */
@Composable @Composable
private fun createDarkColorScheme() = darkColorScheme( private fun createDarkColorScheme() = darkColorScheme(
primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f), primary = ThemeConfig.currentTheme.primaryDark,
onPrimary = Color.White, onPrimary = ThemeConfig.currentTheme.onPrimaryDark,
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f), primaryContainer = ThemeConfig.currentTheme.primaryContainerDark,
onPrimaryContainer = Color.White, onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerDark,
secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f), secondary = ThemeConfig.currentTheme.secondaryDark,
onSecondary = Color.White, onSecondary = ThemeConfig.currentTheme.onSecondaryDark,
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f), secondaryContainer = ThemeConfig.currentTheme.secondaryContainerDark,
onSecondaryContainer = Color.White, onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerDark,
tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f), tertiary = ThemeConfig.currentTheme.tertiaryDark,
onTertiary = Color.White, onTertiary = ThemeConfig.currentTheme.onTertiaryDark,
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f), tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerDark,
onTertiaryContainer = Color.White, onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerDark,
background = Color.Transparent, error = ThemeConfig.currentTheme.errorDark,
surface = Color.Transparent, onError = ThemeConfig.currentTheme.onErrorDark,
onBackground = Color.White, errorContainer = ThemeConfig.currentTheme.errorContainerDark,
onSurface = Color.White, onErrorContainer = ThemeConfig.currentTheme.onErrorContainerDark,
surfaceVariant = Color(0xFF2F2F2F), background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundDark,
onSurfaceVariant = Color.White.copy(alpha = 0.7f), onBackground = ThemeConfig.currentTheme.onBackgroundDark,
outline = Color.White.copy(alpha = 0.12f), surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceDark,
outlineVariant = Color.White.copy(alpha = 0.12f), onSurface = ThemeConfig.currentTheme.onSurfaceDark,
error = ThemeConfig.currentTheme.Error, surfaceVariant = ThemeConfig.currentTheme.surfaceVariantDark,
onError = ThemeConfig.currentTheme.OnError, onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantDark,
errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f), outline = ThemeConfig.currentTheme.outlineDark,
onErrorContainer = Color.White outlineVariant = ThemeConfig.currentTheme.outlineVariantDark,
scrim = ThemeConfig.currentTheme.scrimDark,
inverseSurface = ThemeConfig.currentTheme.inverseSurfaceDark,
inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceDark,
inversePrimary = ThemeConfig.currentTheme.inversePrimaryDark,
surfaceDim = ThemeConfig.currentTheme.surfaceDimDark,
surfaceBright = ThemeConfig.currentTheme.surfaceBrightDark,
surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestDark,
surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowDark,
surfaceContainer = ThemeConfig.currentTheme.surfaceContainerDark,
surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighDark,
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark,
) )
/** /**
@@ -334,32 +363,44 @@ private fun createDarkColorScheme() = darkColorScheme(
*/ */
@Composable @Composable
private fun createLightColorScheme() = lightColorScheme( private fun createLightColorScheme() = lightColorScheme(
primary = ThemeConfig.currentTheme.Primary, primary = ThemeConfig.currentTheme.primaryLight,
onPrimary = ThemeConfig.currentTheme.OnPrimary, onPrimary = ThemeConfig.currentTheme.onPrimaryLight,
primaryContainer = ThemeConfig.currentTheme.PrimaryContainer, primaryContainer = ThemeConfig.currentTheme.primaryContainerLight,
onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer, onPrimaryContainer = ThemeConfig.currentTheme.onPrimaryContainerLight,
secondary = ThemeConfig.currentTheme.Secondary, secondary = ThemeConfig.currentTheme.secondaryLight,
onSecondary = ThemeConfig.currentTheme.OnSecondary, onSecondary = ThemeConfig.currentTheme.onSecondaryLight,
secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer, secondaryContainer = ThemeConfig.currentTheme.secondaryContainerLight,
onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer, onSecondaryContainer = ThemeConfig.currentTheme.onSecondaryContainerLight,
tertiary = ThemeConfig.currentTheme.Tertiary, tertiary = ThemeConfig.currentTheme.tertiaryLight,
onTertiary = ThemeConfig.currentTheme.OnTertiary, onTertiary = ThemeConfig.currentTheme.onTertiaryLight,
tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer, tertiaryContainer = ThemeConfig.currentTheme.tertiaryContainerLight,
onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer, onTertiaryContainer = ThemeConfig.currentTheme.onTertiaryContainerLight,
background = Color.Transparent, error = ThemeConfig.currentTheme.errorLight,
surface = Color.Transparent, onError = ThemeConfig.currentTheme.onErrorLight,
onBackground = Color.Black.copy(alpha = 0.87f), errorContainer = ThemeConfig.currentTheme.errorContainerLight,
onSurface = Color.Black.copy(alpha = 0.87f), onErrorContainer = ThemeConfig.currentTheme.onErrorContainerLight,
surfaceVariant = Color(0xFFF5F5F5), background = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.backgroundLight,
onSurfaceVariant = Color.Black.copy(alpha = 0.78f), onBackground = ThemeConfig.currentTheme.onBackgroundLight,
outline = Color.Black.copy(alpha = 0.12f), surface = if (CardConfig.isCustomBackgroundEnabled) Color.Transparent else ThemeConfig.currentTheme.surfaceLight,
outlineVariant = Color.Black.copy(alpha = 0.12f), onSurface = ThemeConfig.currentTheme.onSurfaceLight,
error = ThemeConfig.currentTheme.Error, surfaceVariant = ThemeConfig.currentTheme.surfaceVariantLight,
onError = ThemeConfig.currentTheme.OnError, onSurfaceVariant = ThemeConfig.currentTheme.onSurfaceVariantLight,
errorContainer = ThemeConfig.currentTheme.ErrorContainer, outline = ThemeConfig.currentTheme.outlineLight,
onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer outlineVariant = ThemeConfig.currentTheme.outlineVariantLight,
scrim = ThemeConfig.currentTheme.scrimLight,
inverseSurface = ThemeConfig.currentTheme.inverseSurfaceLight,
inverseOnSurface = ThemeConfig.currentTheme.inverseOnSurfaceLight,
inversePrimary = ThemeConfig.currentTheme.inversePrimaryLight,
surfaceDim = ThemeConfig.currentTheme.surfaceDimLight,
surfaceBright = ThemeConfig.currentTheme.surfaceBrightLight,
surfaceContainerLowest = ThemeConfig.currentTheme.surfaceContainerLowestLight,
surfaceContainerLow = ThemeConfig.currentTheme.surfaceContainerLowLight,
surfaceContainer = ThemeConfig.currentTheme.surfaceContainerLight,
surfaceContainerHigh = ThemeConfig.currentTheme.surfaceContainerHighLight,
surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight,
) )
/** /**
* 复制图片到应用内部存储并提升持久性 * 复制图片到应用内部存储并提升持久性
*/ */
@@ -546,9 +587,6 @@ fun Context.loadDynamicColorState() {
ThemeConfig.useDynamicColor = enabled ThemeConfig.useDynamicColor = enabled
} }
/**
* webui X样式
*/
@Composable @Composable
private fun SystemBarStyle( private fun SystemBarStyle(
darkMode: Boolean, darkMode: Boolean,

View File

@@ -7,13 +7,23 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.sukisu.ultra.ui.util.module.LatestVersionInfo import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import androidx.core.net.toUri import androidx.core.net.toUri
import java.io.File
import java.util.concurrent.TimeUnit
private const val TAG = "DownloadUtil"
private val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0 (Linux; Android ${Build.VERSION.RELEASE}; ${Build.MODEL})"
private const val MAX_RETRY_COUNT = 3
private const val RETRY_DELAY_MS = 3000L
/** /**
* @author weishu * @author weishu
@@ -26,8 +36,10 @@ fun download(
fileName: String, fileName: String,
description: String, description: String,
onDownloaded: (Uri) -> Unit = {}, onDownloaded: (Uri) -> Unit = {},
onDownloading: () -> Unit = {} onDownloading: () -> Unit = {},
onError: (String) -> Unit = {}
) { ) {
Log.d(TAG, "Start Download: $url")
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val query = DownloadManager.Query() val query = DownloadManager.Query()
@@ -49,6 +61,13 @@ fun download(
} }
} }
} }
val downloadFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
if (downloadFile.exists()) {
downloadFile.delete()
}
val request = DownloadManager.Request(url.toUri()) val request = DownloadManager.Request(url.toUri())
.setDestinationInExternalPublicDir( .setDestinationInExternalPublicDir(
@@ -59,66 +78,206 @@ fun download(
.setMimeType("application/zip") .setMimeType("application/zip")
.setTitle(fileName) .setTitle(fileName)
.setDescription(description) .setDescription(description)
.addRequestHeader("User-Agent", CUSTOM_USER_AGENT)
.setAllowedOverMetered(true)
.setAllowedOverRoaming(true)
.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
downloadManager.enqueue(request) try {
val downloadId = downloadManager.enqueue(request)
Log.d(TAG, "Successful launch of the downloadID: $downloadId")
monitorDownload(context, downloadManager, downloadId, url, fileName, description, onDownloaded, onDownloading, onError)
} catch (e: Exception) {
Log.e(TAG, "Download startup failure", e)
onError("Download startup failure: ${e.message}")
}
}
private fun monitorDownload(
context: Context,
downloadManager: DownloadManager,
downloadId: Long,
url: String,
fileName: String,
description: String,
onDownloaded: (Uri) -> Unit,
onDownloading: () -> Unit,
onError: (String) -> Unit,
retryCount: Int = 0
) {
val handler = Handler(Looper.getMainLooper())
val query = DownloadManager.Query().setFilterById(downloadId)
var lastProgress = -1
var stuckCounter = 0
val runnable = object : Runnable {
override fun run() {
downloadManager.query(query).use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
@SuppressLint("Range")
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
when (status) {
DownloadManager.STATUS_SUCCESSFUL -> {
@SuppressLint("Range")
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
Log.d(TAG, "Download Successfully: $localUri")
onDownloaded(localUri.toUri())
return
}
DownloadManager.STATUS_FAILED -> {
@SuppressLint("Range")
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
Log.d(TAG, "Download failed with reason code: $reason")
if (retryCount < MAX_RETRY_COUNT) {
Log.d(TAG, "Attempts to re download, number of retries: ${retryCount + 1}")
handler.postDelayed({
downloadManager.remove(downloadId)
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
}, RETRY_DELAY_MS)
} else {
onError("Download failed, please check network connection or storage space")
}
return
}
DownloadManager.STATUS_RUNNING, DownloadManager.STATUS_PENDING, DownloadManager.STATUS_PAUSED -> {
@SuppressLint("Range")
val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
@SuppressLint("Range")
val downloadedBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
if (totalBytes > 0) {
val progress = (downloadedBytes * 100 / totalBytes).toInt()
if (progress == lastProgress) {
stuckCounter++
if (stuckCounter > 30) {
if (retryCount < MAX_RETRY_COUNT) {
Log.d(TAG, "Download stalled and restarted")
downloadManager.remove(downloadId)
download(context, url, fileName, description, onDownloaded, onDownloading, onError)
return
}
}
} else {
lastProgress = progress
stuckCounter = 0
Log.d(TAG, "Download progress: $progress% ($downloadedBytes/$totalBytes)")
}
}
}
}
}
}
handler.postDelayed(this, 1000)
}
}
handler.post(runnable)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val id = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) ?: -1
if (id == downloadId) {
handler.removeCallbacks(runnable)
val query = DownloadManager.Query().setFilterById(downloadId)
downloadManager.query(query).use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
@SuppressLint("Range")
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
if (status == DownloadManager.STATUS_SUCCESSFUL) {
@SuppressLint("Range")
val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
onDownloaded(localUri.toUri())
} else {
if (retryCount < MAX_RETRY_COUNT) {
download(context!!, url, fileName, description, onDownloaded, onDownloading, onError)
} else {
onError("Download failed, please try again later")
}
}
}
}
context?.unregisterReceiver(this)
}
}
}
ContextCompat.registerReceiver(
context,
receiver,
IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE),
ContextCompat.RECEIVER_EXPORTED
)
} }
fun checkNewVersion(): LatestVersionInfo { fun checkNewVersion(): LatestVersionInfo {
val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest" val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest"
val defaultValue = LatestVersionInfo() val defaultValue = LatestVersionInfo()
return runCatching { return runCatching {
okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute() val client = okhttp3.OkHttpClient.Builder()
.use { response -> .connectTimeout(15, TimeUnit.SECONDS)
if (!response.isSuccessful) { .readTimeout(30, TimeUnit.SECONDS)
Log.d("CheckUpdate", "Network request failed: ${response.message}") .writeTimeout(15, TimeUnit.SECONDS)
return defaultValue .build()
}
val body = response.body?.string()
if (body == null) {
Log.d("CheckUpdate", "Response body is null")
return defaultValue
}
Log.d("CheckUpdate", "Response body: $body")
val json = org.json.JSONObject(body)
// 直接从 tag_name 提取版本号(如 v1.1 val request = okhttp3.Request.Builder()
val tagName = json.optString("tag_name", "") .url(url)
val versionName = tagName.removePrefix("v") // 移除前缀 "v" .header("User-Agent", CUSTOM_USER_AGENT)
.build()
// 从 body 字段获取更新日志(保留换行符) client.newCall(request).execute().use { response ->
val changelog = json.optString("body") if (!response.isSuccessful) {
.replace("\\r\\n", "\n") // 转换换行符 Log.d("CheckUpdate", "Network request failed: ${response.message}")
return defaultValue
val assets = json.getJSONArray("assets")
for (i in 0 until assets.length()) {
val asset = assets.getJSONObject(i)
val name = asset.getString("name")
if (!name.endsWith(".apk")) continue
// 修改正则表达式,只匹配 SukiSU 和版本号
val regex = Regex("SukiSU.*_(\\d+)-release")
val matchResult = regex.find(name)
if (matchResult == null) {
Log.d("CheckUpdate", "No match found in $name, skipping")
continue
}
val versionCode = matchResult.groupValues[1].toInt()
val downloadUrl = asset.getString("browser_download_url")
return LatestVersionInfo(
versionCode,
downloadUrl,
changelog,
versionName
)
}
Log.d("CheckUpdate", "No valid apk asset found, returning default value")
defaultValue
} }
val body = response.body?.string()
if (body == null) {
Log.d("CheckUpdate", "Return data is null")
return defaultValue
}
Log.d("CheckUpdate", "Return data: $body")
val json = org.json.JSONObject(body)
// 直接从 tag_name 提取版本号(如 v1.1
val tagName = json.optString("tag_name", "")
val versionName = tagName.removePrefix("v") // 移除前缀 "v"
// 从 body 字段获取更新日志(保留换行符)
val changelog = json.optString("body")
.replace("\\r\\n", "\n") // 转换换行符
val assets = json.getJSONArray("assets")
for (i in 0 until assets.length()) {
val asset = assets.getJSONObject(i)
val name = asset.getString("name")
if (!name.endsWith(".apk")) continue
val regex = Regex("SukiSU.*_(\\d+)-release")
val matchResult = regex.find(name)
if (matchResult == null) {
Log.d("CheckUpdate", "No matches found: $name, skip over")
continue
}
val versionCode = matchResult.groupValues[1].toInt()
val downloadUrl = asset.getString("browser_download_url")
return LatestVersionInfo(
versionCode,
downloadUrl,
changelog,
versionName
)
}
Log.d("CheckUpdate", "No valid APK resource found, return default value")
defaultValue
}
}.getOrDefault(defaultValue) }.getOrDefault(defaultValue)
} }
@Composable @Composable
fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) { fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
DisposableEffect(context) { DisposableEffect(context) {
@@ -157,5 +316,4 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) {
context.unregisterReceiver(receiver) context.unregisterReceiver(receiver)
} }
} }
} }

View File

@@ -76,9 +76,9 @@ fun createRootShell(globalMnt: Boolean = false): Shell {
Log.w(TAG, "ksu failed: ", e) Log.w(TAG, "ksu failed: ", e)
try { try {
if (globalMnt) { if (globalMnt) {
builder.build("su")
} else {
builder.build("su", "-mm") builder.build("su", "-mm")
} else {
builder.build("su")
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, "su failed: ", e) Log.e(TAG, "su failed: ", e)

View File

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

View File

@@ -0,0 +1,105 @@
package com.sukisu.ultra.ui.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import java.io.BufferedReader
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.util.zip.ZipInputStream
import com.sukisu.ultra.R
import android.util.Log
import java.io.IOException
object ModuleUtils {
private const val TAG = "ModuleUtils"
fun extractModuleName(context: Context, uri: Uri): String {
if (uri == Uri.EMPTY) {
Log.e(TAG, "The supplied URI is empty")
return context.getString(R.string.unknown_module)
}
return try {
Log.d(TAG, "Start extracting module names from URIs: $uri")
// 从URI路径中提取文件名
val fileName = uri.lastPathSegment?.let { path ->
val lastSlash = path.lastIndexOf('/')
if (lastSlash != -1 && lastSlash < path.length - 1) {
path.substring(lastSlash + 1)
} else {
path
}
}?.removeSuffix(".zip") ?: context.getString(R.string.unknown_module)
var formattedFileName = fileName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
var moduleName = formattedFileName
try {
// 打开ZIP文件输入流
val inputStream = context.contentResolver.openInputStream(uri)
if (inputStream == null) {
Log.e(TAG, "Unable to get input stream from URI: $uri")
return formattedFileName
}
val zipInputStream = ZipInputStream(inputStream)
var entry = zipInputStream.nextEntry
// 遍历ZIP文件中的条目查找module.prop文件
while (entry != null) {
if (entry.name == "module.prop") {
val reader = BufferedReader(InputStreamReader(zipInputStream, StandardCharsets.UTF_8))
var line: String?
var nameFound = false
while (reader.readLine().also { line = it } != null) {
if (line?.startsWith("name=") == true) {
moduleName = line.substringAfter("=")
moduleName = moduleName.replace(Regex("[^a-zA-Z0-9\\s\\-_.@()\\u4e00-\\u9fa5]"), "").trim()
nameFound = true
break
}
}
break
}
entry = zipInputStream.nextEntry
}
zipInputStream.close()
Log.d(TAG, "Successfully extracted module name: $moduleName")
moduleName
} catch (e: IOException) {
Log.e(TAG, "Error reading ZIP file: ${e.message}")
formattedFileName
}
} catch (e: Exception) {
Log.e(TAG, "Exception when extracting module name: ${e.message}")
context.getString(R.string.unknown_module)
}
}
// 验证URI是否有效并可访问
fun isUriAccessible(context: Context, uri: Uri): Boolean {
if (uri == Uri.EMPTY) return false
return try {
val inputStream = context.contentResolver.openInputStream(uri)
inputStream?.close()
inputStream != null
} catch (e: Exception) {
Log.e(TAG, "The URI is inaccessible: $uri, Error: ${e.message}")
false
}
}
// 获取URI的持久权限
fun takePersistableUriPermission(context: Context, uri: Uri) {
try {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
context.contentResolver.takePersistableUriPermission(uri, flags)
Log.d(TAG, "Persistent permissions for URIs have been obtained: $uri")
} catch (e: Exception) {
Log.e(TAG, "Unable to get persistent permissions on URIs: $uri, Error: ${e.message}")
}
}
}

View File

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

View File

@@ -1,12 +1,10 @@
package com.sukisu.ultra.ui.util package com.sukisu.ultra.ui.util
import androidx.compose.runtime.Composable import android.content.Context
import androidx.compose.ui.res.stringResource
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.sukisu.ultra.R import com.sukisu.ultra.R
@Composable fun getSELinuxStatus(context: Context): String {
fun getSELinuxStatus(): String {
val shell = Shell.Builder.create().build("sh") val shell = Shell.Builder.create().build("sh")
val list = ArrayList<String>() val list = ArrayList<String>()
@@ -18,16 +16,16 @@ fun getSELinuxStatus(): String {
return if (result.isSuccess) { return if (result.isSuccess) {
when (output) { when (output) {
"Enforcing" -> stringResource(R.string.selinux_status_enforcing) "Enforcing" -> context.getString(R.string.selinux_status_enforcing)
"Permissive" -> stringResource(R.string.selinux_status_permissive) "Permissive" -> context.getString(R.string.selinux_status_permissive)
"Disabled" -> stringResource(R.string.selinux_status_disabled) "Disabled" -> context.getString(R.string.selinux_status_disabled)
else -> stringResource(R.string.selinux_status_unknown) else -> context.getString(R.string.selinux_status_unknown)
} }
} else { } else {
if (output.contains("Permission denied")) { if (output.contains("Permission denied")) {
stringResource(R.string.selinux_status_enforcing) context.getString(R.string.selinux_status_enforcing)
} else { } else {
stringResource(R.string.selinux_status_unknown) context.getString(R.string.selinux_status_unknown)
} }
} }
} }

View File

@@ -0,0 +1,674 @@
package com.sukisu.ultra.ui.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.widget.Toast
import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.sukisu.ultra.Natives
import com.sukisu.ultra.R
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import androidx.core.content.edit
/**
* SuSFS 配置管理器
* 用于管理SuSFS相关的配置和命令执行
*/
object SuSFSManager {
private const val PREFS_NAME = "susfs_config"
private const val KEY_UNAME_VALUE = "uname_value"
private const val KEY_BUILD_TIME_VALUE = "build_time_value"
private const val KEY_AUTO_START_ENABLED = "auto_start_enabled"
private const val KEY_SUS_PATHS = "sus_paths"
private const val KEY_SUS_MOUNTS = "sus_mounts"
private const val KEY_TRY_UMOUNTS = "try_umounts"
private const val KEY_ANDROID_DATA_PATH = "android_data_path"
private const val KEY_SDCARD_PATH = "sdcard_path"
private const val KEY_ENABLE_LOG = "enable_log"
private const val KEY_EXECUTE_IN_POST_FS_DATA = "execute_in_post_fs_data"
private const val KEY_KSTAT_CONFIGS = "kstat_configs"
private const val KEY_ADD_KSTAT_PATHS = "add_kstat_paths"
private const val KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS = "hide_sus_mounts_for_all_procs"
// 常量
private const val SUSFS_BINARY_BASE_NAME = "ksu_susfs"
private const val DEFAULT_UNAME = "default"
private const val DEFAULT_BUILD_TIME = "default"
private const val MODULE_ID = "susfs_manager"
private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID"
data class SlotInfo(val slotName: String, val uname: String, val buildTime: String)
data class CommandResult(val isSuccess: Boolean, val output: String, val errorOutput: String = "")
data class EnabledFeature(
val name: String,
val isEnabled: Boolean,
val statusText: String = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled),
val canConfigure: Boolean = false
)
// 命令执行
private fun getPrefs(context: Context): SharedPreferences =
context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private fun getSuSFSVersionUse(): String = try {
getSuSFSVersion()
} catch (_: Exception) { "1.5.8" }
private fun getSuSFSBinaryName(): String = "${SUSFS_BINARY_BASE_NAME}_${getSuSFSVersionUse().removePrefix("v")}"
private fun getSuSFSTargetPath(): String = "/data/adb/ksu/bin/${getSuSFSBinaryName()}"
private fun runCmd(shell: Shell, cmd: String): String {
return shell.newJob()
.add(cmd)
.to(mutableListOf<String>(), null)
.exec().out
.joinToString("\n")
}
private fun runCmdWithResult(cmd: String): CommandResult {
val result = Shell.getShell().newJob().add(cmd).exec()
return CommandResult(result.isSuccess, result.out.joinToString("\n"), result.err.joinToString("\n"))
}
// 版本比较
private fun compareVersions(version1: String, version2: String): Int {
val v1Parts = version1.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
val v2Parts = version2.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
val maxLength = maxOf(v1Parts.size, v2Parts.size)
for (i in 0 until maxLength) {
val v1Part = v1Parts.getOrNull(i) ?: 0
val v2Part = v2Parts.getOrNull(i) ?: 0
when {
v1Part > v2Part -> return 1
v1Part < v2Part -> return -1
}
}
return 0
}
// 检查当前SuSFS版本是否支持SUS挂载隐藏控制功能
fun isSusVersion_1_5_8(): Boolean {
return try {
val currentVersion = getSuSFSVersion()
compareVersions(currentVersion, "1.5.8") >= 0
} catch (_: Exception) {
true
}
}
// 配置存取方法
fun saveUnameValue(context: Context, value: String) =
getPrefs(context).edit { putString(KEY_UNAME_VALUE, value) }
fun getUnameValue(context: Context): String =
getPrefs(context).getString(KEY_UNAME_VALUE, DEFAULT_UNAME) ?: DEFAULT_UNAME
fun saveBuildTimeValue(context: Context, value: String) =
getPrefs(context).edit { putString(KEY_BUILD_TIME_VALUE, value)}
fun getBuildTimeValue(context: Context): String =
getPrefs(context).getString(KEY_BUILD_TIME_VALUE, DEFAULT_BUILD_TIME) ?: DEFAULT_BUILD_TIME
fun getLastAppliedValue(context: Context): String = getUnameValue(context)
fun getLastAppliedBuildTime(context: Context): String = getBuildTimeValue(context)
fun setAutoStartEnabled(context: Context, enabled: Boolean) =
getPrefs(context).edit { putBoolean(KEY_AUTO_START_ENABLED, enabled) }
fun isAutoStartEnabled(context: Context): Boolean =
getPrefs(context).getBoolean(KEY_AUTO_START_ENABLED, false)
fun saveEnableLogState(context: Context, enabled: Boolean) =
getPrefs(context).edit { putBoolean(KEY_ENABLE_LOG, enabled) }
fun getEnableLogState(context: Context): Boolean =
getPrefs(context).getBoolean(KEY_ENABLE_LOG, false)
fun getExecuteInPostFsData(context: Context): Boolean =
getPrefs(context).getBoolean(KEY_EXECUTE_IN_POST_FS_DATA, false)
fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) {
getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) }
if (isAutoStartEnabled(context)) {
kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch {
removeMagiskModule()
createMagiskModule(context)
}
}
}
// SUS挂载隐藏控制
fun saveHideSusMountsForAllProcs(context: Context, hideForAll: Boolean) =
getPrefs(context).edit { putBoolean(KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS, hideForAll) }
fun getHideSusMountsForAllProcs(context: Context): Boolean =
getPrefs(context).getBoolean(KEY_HIDE_SUS_MOUNTS_FOR_ALL_PROCS, true)
// 路径和配置管理
fun saveSusPaths(context: Context, paths: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_SUS_PATHS, paths) }
fun getSusPaths(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SUS_PATHS, emptySet()) ?: emptySet()
fun saveSusMounts(context: Context, mounts: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) }
fun getSusMounts(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_SUS_MOUNTS, emptySet()) ?: emptySet()
fun saveTryUmounts(context: Context, umounts: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_TRY_UMOUNTS, umounts) }
fun getTryUmounts(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_TRY_UMOUNTS, emptySet()) ?: emptySet()
fun saveKstatConfigs(context: Context, configs: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_KSTAT_CONFIGS, configs) }
fun getKstatConfigs(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_KSTAT_CONFIGS, emptySet()) ?: emptySet()
fun saveAddKstatPaths(context: Context, paths: Set<String>) =
getPrefs(context).edit { putStringSet(KEY_ADD_KSTAT_PATHS, paths) }
fun getAddKstatPaths(context: Context): Set<String> =
getPrefs(context).getStringSet(KEY_ADD_KSTAT_PATHS, emptySet()) ?: emptySet()
@SuppressLint("SdCardPath")
fun saveAndroidDataPath(context: Context, path: String) =
getPrefs(context).edit { putString(KEY_ANDROID_DATA_PATH, path) }
@SuppressLint("SdCardPath")
fun getAndroidDataPath(context: Context): String =
getPrefs(context).getString(KEY_ANDROID_DATA_PATH, "/sdcard/Android/data") ?: "/sdcard/Android/data"
@SuppressLint("SdCardPath")
fun saveSdcardPath(context: Context, path: String) =
getPrefs(context).edit { putString(KEY_SDCARD_PATH, path) }
@SuppressLint("SdCardPath")
fun getSdcardPath(context: Context): String =
getPrefs(context).getString(KEY_SDCARD_PATH, "/sdcard") ?: "/sdcard"
// 槽位信息获取
suspend fun getCurrentSlotInfo(): List<SlotInfo> = withContext(Dispatchers.IO) {
try {
val slotInfoList = mutableListOf<SlotInfo>()
val shell = Shell.getShell()
listOf("boot_a", "boot_b").forEach { slot ->
val unameCmd =
"strings -n 20 /dev/block/by-name/$slot | awk '/Linux version/ && ++c==2 {print $3; exit}'"
val buildTimeCmd = "strings -n 20 /dev/block/by-name/$slot | sed -n '/Linux version.*#/{s/.*#/#/p;q}'"
val uname = runCmd(shell, unameCmd).trim()
val buildTime = runCmd(shell, buildTimeCmd).trim()
if (uname.isNotEmpty() && buildTime.isNotEmpty()) {
slotInfoList.add(SlotInfo(slot, uname.ifEmpty { "unknown" }, buildTime.ifEmpty { "unknown" }))
}
}
slotInfoList
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
}
suspend fun getCurrentActiveSlot(): String = withContext(Dispatchers.IO) {
try {
val shell = Shell.getShell()
val suffix = runCmd(shell, "getprop ro.boot.slot_suffix").trim()
when (suffix) {
"_a" -> "boot_a"
"_b" -> "boot_b"
else -> "unknown"
}
} catch (_: Exception) {
"unknown"
}
}
// 二进制文件管理
private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) {
try {
val binaryName = getSuSFSBinaryName()
val targetPath = getSuSFSTargetPath()
val tempFile = File(context.cacheDir, binaryName)
context.assets.open(binaryName).use { input ->
FileOutputStream(tempFile).use { output ->
input.copyTo(output)
}
}
val success = runCmdWithResult("cp '${tempFile.absolutePath}' '$targetPath' && chmod 755 '$targetPath'").isSuccess
tempFile.delete()
if (success && runCmdWithResult("test -f '$targetPath'").isSuccess) targetPath else null
} catch (e: IOException) {
e.printStackTrace()
null
}
}
fun isBinaryAvailable(context: Context): Boolean = try {
context.assets.open(getSuSFSBinaryName()).use { true }
} catch (_: IOException) { false }
// 命令执行
private suspend fun executeSusfsCommand(context: Context, command: String): Boolean = withContext(Dispatchers.IO) {
try {
val binaryPath = copyBinaryFromAssets(context) ?: run {
showToast(context, context.getString(R.string.susfs_binary_not_found))
return@withContext false
}
val result = runCmdWithResult("$binaryPath $command")
if (!result.isSuccess) {
showToast(context, "${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}")
}
result.isSuccess
} catch (e: Exception) {
e.printStackTrace()
showToast(context, context.getString(R.string.susfs_command_error, e.message ?: "Unknown error"))
false
}
}
private suspend fun executeSusfsCommandWithOutput(context: Context, command: String): CommandResult = withContext(Dispatchers.IO) {
try {
val binaryPath = copyBinaryFromAssets(context) ?: return@withContext CommandResult(
false, "", context.getString(R.string.susfs_binary_not_found)
)
runCmdWithResult("$binaryPath $command")
} catch (e: Exception) {
e.printStackTrace()
CommandResult(false, "", e.message ?: "Unknown error")
}
}
private suspend fun showToast(context: Context, message: String) = withContext(Dispatchers.Main) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
private inline fun <reified T> Map<String, Any?>.getSetSafe(key: String): Set<T> {
return when (val value = this[key]) {
is Set<*> -> value.filterIsInstance<T>().toSet()
else -> emptySet()
}
}
// 模块管理
private suspend fun createMagiskModule(context: Context): Boolean = withContext(Dispatchers.IO) {
try {
val targetPath = getSuSFSTargetPath()
// 创建模块目录
if (!runCmdWithResult("mkdir -p $MODULE_PATH").isSuccess) return@withContext false
// 创建module.prop
val moduleProp = ScriptGenerator.generateModuleProp(MODULE_ID)
if (!runCmdWithResult("cat > $MODULE_PATH/module.prop << 'EOF'\n$moduleProp\nEOF").isSuccess) return@withContext false
// 获取配置
val config = mapOf(
"unameValue" to getUnameValue(context),
"buildTimeValue" to getBuildTimeValue(context),
"executeInPostFsData" to getExecuteInPostFsData(context),
"susPaths" to getSusPaths(context),
"susMounts" to getSusMounts(context),
"tryUmounts" to getTryUmounts(context),
"androidDataPath" to getAndroidDataPath(context),
"sdcardPath" to getSdcardPath(context),
"enableLog" to getEnableLogState(context),
"kstatConfigs" to getKstatConfigs(context),
"addKstatPaths" to getAddKstatPaths(context),
"hideSusMountsForAllProcs" to getHideSusMountsForAllProcs(context)
)
// 生成脚本
val scripts = mapOf(
"service.sh" to ScriptGenerator.generateServiceScript(
targetPath, config["unameValue"] as String, config["buildTimeValue"] as String,
config.getSetSafe<String>("susPaths"), config["androidDataPath"] as String,
config["sdcardPath"] as String, config["enableLog"] as Boolean,
config["executeInPostFsData"] as Boolean, config.getSetSafe<String>("kstatConfigs"),
config.getSetSafe<String>("addKstatPaths")
),
"post-fs-data.sh" to ScriptGenerator.generatePostFsDataScript(
targetPath, config["unameValue"] as String, config["buildTimeValue"] as String,
config["executeInPostFsData"] as Boolean
),
"post-mount.sh" to ScriptGenerator.generatePostMountScript(
targetPath, config.getSetSafe<String>("susMounts"), config.getSetSafe<String>("tryUmounts")
),
"boot-completed.sh" to ScriptGenerator.generateBootCompletedScript(
targetPath, config["hideSusMountsForAllProcs"] as Boolean
)
)
// 创建脚本文件
scripts.all { (filename, content) ->
runCmdWithResult("cat > $MODULE_PATH/$filename << 'EOF'\n$content\nEOF").isSuccess &&
runCmdWithResult("chmod 755 $MODULE_PATH/$filename").isSuccess
}
} catch (e: Exception) {
e.printStackTrace()
false
}
}
private suspend fun removeMagiskModule(): Boolean = withContext(Dispatchers.IO) {
try {
runCmdWithResult("rm -rf $MODULE_PATH").isSuccess
} catch (e: Exception) {
e.printStackTrace()
false
}
}
// 功能状态获取
suspend fun getEnabledFeatures(context: Context): List<EnabledFeature> = withContext(Dispatchers.IO) {
try {
Natives.getSusfsFeatureStatus()?.let { status ->
parseEnabledFeaturesFromStatus(context, status)
} ?: emptyList()
} catch (e: Exception) {
e.printStackTrace()
emptyList()
}
}
private fun parseEnabledFeaturesFromStatus(context: Context, status: Natives.SusfsFeatureStatus): List<EnabledFeature> {
val featureList = listOf(
Triple("status_sus_path", context.getString(R.string.sus_path_feature_label), status.statusSusPath),
Triple("status_sus_mount", context.getString(R.string.sus_mount_feature_label), status.statusSusMount),
Triple("status_try_umount", context.getString(R.string.try_umount_feature_label), status.statusTryUmount),
Triple("status_spoof_uname", context.getString(R.string.spoof_uname_feature_label), status.statusSpoofUname),
Triple("status_spoof_cmdline", context.getString(R.string.spoof_cmdline_feature_label), status.statusSpoofCmdline),
Triple("status_open_redirect", context.getString(R.string.open_redirect_feature_label), status.statusOpenRedirect),
Triple("status_enable_log", context.getString(R.string.enable_log_feature_label), status.statusEnableLog),
Triple("status_auto_default_mount", context.getString(R.string.auto_default_mount_feature_label), status.statusAutoDefaultMount),
Triple("status_auto_bind_mount", context.getString(R.string.auto_bind_mount_feature_label), status.statusAutoBindMount),
Triple("status_auto_try_umount_bind", context.getString(R.string.auto_try_umount_bind_feature_label), status.statusAutoTryUmountBind),
Triple("status_hide_symbols", context.getString(R.string.hide_symbols_feature_label), status.statusHideSymbols),
Triple("status_sus_kstat", context.getString(R.string.sus_kstat_feature_label), status.statusSusKstat),
Triple("status_magic_mount", context.getString(R.string.magic_mount_feature_label), status.statusMagicMount),
Triple("status_sus_su", context.getString(R.string.sus_su_feature_label), status.statusSusSu)
)
return featureList.map { (id, displayName, isEnabled) ->
val statusText = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled)
val canConfigure = id == "status_enable_log"
EnabledFeature(displayName, isEnabled, statusText, canConfigure)
}.sortedBy { it.name }
}
// sus日志开关
suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean {
val success = executeSusfsCommand(context, "enable_log ${if (enabled) 1 else 0}")
if (success) {
saveEnableLogState(context, enabled)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, if (enabled) context.getString(R.string.susfs_log_enabled) else context.getString(R.string.susfs_log_disabled))
}
return success
}
// SUS挂载隐藏控制
suspend fun setHideSusMountsForAllProcs(context: Context, hideForAll: Boolean): Boolean {
if (!isSusVersion_1_5_8()) {
return false
}
val success = executeSusfsCommand(context, "hide_sus_mnts_for_all_procs ${if (hideForAll) 1 else 0}")
if (success) {
saveHideSusMountsForAllProcs(context, hideForAll)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, if (hideForAll)
context.getString(R.string.susfs_hide_mounts_all_enabled)
else
context.getString(R.string.susfs_hide_mounts_all_disabled)
)
}
return success
}
// uname和构建时间
@SuppressLint("StringFormatMatches")
suspend fun setUname(context: Context, unameValue: String, buildTimeValue: String): Boolean {
val success = executeSusfsCommand(context, "set_uname '$unameValue' '$buildTimeValue'")
if (success) {
saveUnameValue(context, unameValue)
saveBuildTimeValue(context, buildTimeValue)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, context.getString(R.string.susfs_uname_set_success, unameValue, buildTimeValue))
}
return success
}
// 添加SUS路径
@SuppressLint("StringFormatInvalid")
suspend fun addSusPath(context: Context, path: String): Boolean {
val result = executeSusfsCommandWithOutput(context, "add_sus_path '$path'")
val isActuallySuccessful = result.isSuccess && !result.output.contains("not found, skip adding")
if (isActuallySuccessful) {
saveSusPaths(context, getSusPaths(context) + path)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, context.getString(R.string.susfs_sus_path_added_success, path))
} else {
val errorMessage = if (result.output.contains("not found, skip adding")) {
context.getString(R.string.susfs_path_not_found_error, path)
} else {
"${context.getString(R.string.susfs_command_failed)}\n${result.output}\n${result.errorOutput}"
}
showToast(context, errorMessage)
}
return isActuallySuccessful
}
suspend fun removeSusPath(context: Context, path: String): Boolean {
saveSusPaths(context, getSusPaths(context) - path)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, "SUS path removed: $path")
return true
}
// 添加SUS挂载
suspend fun addSusMount(context: Context, mount: String): Boolean {
val success = executeSusfsCommand(context, "add_sus_mount '$mount'")
if (success) {
saveSusMounts(context, getSusMounts(context) + mount)
if (isAutoStartEnabled(context)) createMagiskModule(context)
}
return success
}
suspend fun removeSusMount(context: Context, mount: String): Boolean {
saveSusMounts(context, getSusMounts(context) - mount)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, "Removed SUS mount: $mount")
return true
}
// 添加尝试卸载
suspend fun addTryUmount(context: Context, path: String, mode: Int): Boolean {
val commandSuccess = executeSusfsCommand(context, "add_try_umount '$path' $mode")
saveTryUmounts(context, getTryUmounts(context) + "$path|$mode")
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, if (commandSuccess) {
context.getString(R.string.susfs_try_umount_added_success, path)
} else {
context.getString(R.string.susfs_try_umount_added_saved, path)
})
return true
}
suspend fun removeTryUmount(context: Context, umountEntry: String): Boolean {
saveTryUmounts(context, getTryUmounts(context) - umountEntry)
if (isAutoStartEnabled(context)) createMagiskModule(context)
val path = umountEntry.split("|").firstOrNull() ?: umountEntry
showToast(context, "Removed Try to uninstall: $path")
return true
}
suspend fun runTryUmount(context: Context): Boolean = executeSusfsCommand(context, "run_try_umount")
// 添加kstat配置
suspend fun addKstatStatically(context: Context, path: String, ino: String, dev: String, nlink: String,
size: String, atime: String, atimeNsec: String, mtime: String, mtimeNsec: String,
ctime: String, ctimeNsec: String, blocks: String, blksize: String): Boolean {
val command = "add_sus_kstat_statically '$path' '$ino' '$dev' '$nlink' '$size' '$atime' '$atimeNsec' '$mtime' '$mtimeNsec' '$ctime' '$ctimeNsec' '$blocks' '$blksize'"
val success = executeSusfsCommand(context, command)
if (success) {
val configEntry = "$path|$ino|$dev|$nlink|$size|$atime|$atimeNsec|$mtime|$mtimeNsec|$ctime|$ctimeNsec|$blocks|$blksize"
saveKstatConfigs(context, getKstatConfigs(context) + configEntry)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, context.getString(R.string.kstat_static_config_added, path))
}
return success
}
suspend fun removeKstatConfig(context: Context, config: String): Boolean {
saveKstatConfigs(context, getKstatConfigs(context) - config)
if (isAutoStartEnabled(context)) createMagiskModule(context)
val path = config.split("|").firstOrNull() ?: config
showToast(context, context.getString(R.string.kstat_config_removed, path))
return true
}
// 添加kstat路径
suspend fun addKstat(context: Context, path: String): Boolean {
val success = executeSusfsCommand(context, "add_sus_kstat '$path'")
if (success) {
saveAddKstatPaths(context, getAddKstatPaths(context) + path)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, context.getString(R.string.kstat_path_added, path))
}
return success
}
suspend fun removeAddKstat(context: Context, path: String): Boolean {
saveAddKstatPaths(context, getAddKstatPaths(context) - path)
if (isAutoStartEnabled(context)) createMagiskModule(context)
showToast(context, context.getString(R.string.kstat_path_removed, path))
return true
}
// 更新kstat
suspend fun updateKstat(context: Context, path: String): Boolean {
val success = executeSusfsCommand(context, "update_sus_kstat '$path'")
if (success) showToast(context, context.getString(R.string.kstat_updated, path))
return success
}
// 更新kstat全克隆
suspend fun updateKstatFullClone(context: Context, path: String): Boolean {
val success = executeSusfsCommand(context, "update_sus_kstat_full_clone '$path'")
if (success) showToast(context, context.getString(R.string.kstat_full_clone_updated, path))
return success
}
// 设置Android数据路径和SD卡路径
suspend fun setAndroidDataPath(context: Context, path: String): Boolean {
val success = executeSusfsCommand(context, "set_android_data_root_path '$path'")
if (success) {
saveAndroidDataPath(context, path)
if (isAutoStartEnabled(context)) createMagiskModule(context)
}
return success
}
// 设置SD卡路径
suspend fun setSdcardPath(context: Context, path: String): Boolean {
val success = executeSusfsCommand(context, "set_sdcard_root_path '$path'")
if (success) {
saveSdcardPath(context, path)
if (isAutoStartEnabled(context)) createMagiskModule(context)
}
return success
}
fun hasConfigurationForAutoStart(context: Context): Boolean {
val enabledFeatures = runBlocking { getEnabledFeatures(context) }
return getUnameValue(context) != DEFAULT_UNAME ||
getBuildTimeValue(context) != DEFAULT_BUILD_TIME ||
getSusPaths(context).isNotEmpty() ||
getSusMounts(context).isNotEmpty() ||
getTryUmounts(context).isNotEmpty() ||
getKstatConfigs(context).isNotEmpty() ||
getAddKstatPaths(context).isNotEmpty() ||
enabledFeatures.any { it.isEnabled }
}
suspend fun configureAutoStart(context: Context, enabled: Boolean): Boolean = withContext(Dispatchers.IO) {
try {
if (enabled) {
if (!hasConfigurationForAutoStart(context)) {
showToast(context, context.getString(R.string.susfs_no_config_to_autostart))
return@withContext false
}
val targetPath = getSuSFSTargetPath()
if (!runCmdWithResult("test -f '$targetPath'").isSuccess) {
copyBinaryFromAssets(context) ?: run {
showToast(context, context.getString(R.string.susfs_binary_not_found))
return@withContext false
}
}
val success = createMagiskModule(context)
if (success) {
setAutoStartEnabled(context, true)
showToast(context, context.getString(R.string.susfs_autostart_enabled_success, MODULE_PATH))
} else {
showToast(context, context.getString(R.string.susfs_autostart_enable_failed))
}
success
} else {
val success = removeMagiskModule()
if (success) {
setAutoStartEnabled(context, false)
showToast(context, context.getString(R.string.susfs_autostart_disabled_success))
} else {
showToast(context, context.getString(R.string.susfs_autostart_disable_failed))
}
success
}
} catch (e: Exception) {
e.printStackTrace()
showToast(context, context.getString(R.string.susfs_autostart_error, e.message ?: "Unknown error"))
false
}
}
suspend fun resetToDefault(context: Context): Boolean {
val success = setUname(context, DEFAULT_UNAME, DEFAULT_BUILD_TIME)
if (success && isAutoStartEnabled(context)) {
configureAutoStart(context, false)
}
return success
}
}

View File

@@ -0,0 +1,411 @@
package com.sukisu.ultra.ui.util
import android.annotation.SuppressLint
/**
* Magisk模块脚本生成器
* 用于生成各种启动脚本的内容
*/
object ScriptGenerator {
// 常量定义
@SuppressLint("SdCardPath")
private const val DEFAULT_ANDROID_DATA_PATH = "/sdcard/Android/data"
@SuppressLint("SdCardPath")
private const val DEFAULT_SDCARD_PATH = "/sdcard"
private const val DEFAULT_UNAME = "default"
private const val DEFAULT_BUILD_TIME = "default"
private const val LOG_DIR = "/data/adb/ksu/log"
// 日志相关的通用脚本片段
private fun generateLogSetup(logFileName: String): String = """
# 日志目录
LOG_DIR="$LOG_DIR"
LOG_FILE="${'$'}LOG_DIR/$logFileName"
# 创建日志目录
mkdir -p "${'$'}LOG_DIR"
# 获取当前时间
get_current_time() {
date '+%Y-%m-%d %H:%M:%S'
}
""".trimIndent()
// 二进制文件检查的通用脚本片段
private fun generateBinaryCheck(targetPath: String): String = """
# 检查SuSFS二进制文件
SUSFS_BIN="$targetPath"
if [ ! -f "${'$'}SUSFS_BIN" ]; then
echo "$(get_current_time): SuSFS二进制文件未找到: ${'$'}SUSFS_BIN" >> "${'$'}LOG_FILE"
exit 1
fi
""".trimIndent()
/**
* 生成service.sh脚本内容
*/
@SuppressLint("SdCardPath")
fun generateServiceScript(
targetPath: String,
unameValue: String,
buildTimeValue: String,
susPaths: Set<String>,
androidDataPath: String,
sdcardPath: String,
enableLog: Boolean,
executeInPostFsData: Boolean = false,
kstatConfigs: Set<String> = emptySet(),
addKstatPaths: Set<String> = emptySet()
): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Service Script")
appendLine("# 在系统服务启动后执行")
appendLine()
appendLine(generateLogSetup("susfs_service.log"))
appendLine()
appendLine(generateBinaryCheck(targetPath))
appendLine()
// 设置日志启用状态
generateLogSettingSection(enableLog)
// 设置路径
generatePathSettingSection(androidDataPath, sdcardPath)
// 添加SUS路径
generateSusPathsSection(susPaths)
// 添加Kstat配置
generateKstatSection(kstatConfigs, addKstatPaths)
// 设置uname和构建时间
generateUnameSection(unameValue, buildTimeValue, executeInPostFsData)
// 隐藏BL相关配置
generateHideBlSection()
appendLine("echo \"$(get_current_time): Service脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
private fun StringBuilder.generateLogSettingSection(enableLog: Boolean) {
appendLine("# 设置日志启用状态")
val logValue = if (enableLog) 1 else 0
appendLine("\"${'$'}SUSFS_BIN\" enable_log $logValue")
appendLine("echo \"$(get_current_time): 日志功能设置为: ${if (enableLog) "启用" else "禁用"}\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
private fun StringBuilder.generatePathSettingSection(
androidDataPath: String,
sdcardPath: String
) {
// 设置Android Data路径
if (androidDataPath != DEFAULT_ANDROID_DATA_PATH) {
appendLine("# 设置Android Data路径")
appendLine("\"${'$'}SUSFS_BIN\" set_android_data_root_path '$androidDataPath'")
appendLine("echo \"$(get_current_time): Android Data路径设置为: $androidDataPath\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
// 设置SD卡路径
if (sdcardPath != DEFAULT_SDCARD_PATH) {
appendLine("# 设置SD卡路径")
appendLine("\"${'$'}SUSFS_BIN\" set_sdcard_root_path '$sdcardPath'")
appendLine("echo \"$(get_current_time): SD卡路径设置为: $sdcardPath\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
}
private fun StringBuilder.generateSusPathsSection(susPaths: Set<String>) {
if (susPaths.isNotEmpty()) {
appendLine("# 添加SUS路径")
susPaths.forEach { path ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_path '$path'")
appendLine("echo \"$(get_current_time): 添加SUS路径: $path\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
}
private fun StringBuilder.generateKstatSection(
kstatConfigs: Set<String>,
addKstatPaths: Set<String>
) {
// 添加Kstat路径
if (addKstatPaths.isNotEmpty()) {
appendLine("# 添加Kstat路径")
addKstatPaths.forEach { path ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat '$path'")
appendLine("echo \"$(get_current_time): 添加Kstat路径: $path\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
// 添加Kstat静态配置
if (kstatConfigs.isNotEmpty()) {
appendLine("# 添加Kstat静态配置")
kstatConfigs.forEach { config ->
val parts = config.split("|")
if (parts.size >= 13) {
val path = parts[0]
val params = parts.drop(1).joinToString("' '", "'", "'")
appendLine("\"${'$'}SUSFS_BIN\" add_sus_kstat_statically '$path' $params")
appendLine("echo \"$(get_current_time): 添加Kstat静态配置: $path\" >> \"${'$'}LOG_FILE\"")
appendLine("\"${'$'}SUSFS_BIN\" update_sus_kstat '$path'")
appendLine("echo \"$(get_current_time): 更新Kstat配置: $path\" >> \"${'$'}LOG_FILE\"")
}
}
appendLine()
}
}
private fun StringBuilder.generateUnameSection(
unameValue: String,
buildTimeValue: String,
executeInPostFsData: Boolean
) {
if (!executeInPostFsData && (unameValue != DEFAULT_UNAME || buildTimeValue != DEFAULT_BUILD_TIME)) {
appendLine("# 设置uname和构建时间")
appendLine("\"${'$'}SUSFS_BIN\" set_uname '$unameValue' '$buildTimeValue'")
appendLine("echo \"$(get_current_time): 设置uname为: $unameValue, 构建时间为: $buildTimeValue\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
}
private fun StringBuilder.generateHideBlSection() {
appendLine("# 隐藏BL 来自 Shamiko 脚本")
appendLine(
"""
check_reset_prop() {
local NAME=$1
local EXPECTED=$2
local VALUE=$(resetprop ${'$'}NAME)
[ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || resetprop ${'$'}NAME ${'$'}EXPECTED
}
check_missing_prop() {
local NAME=$1
local EXPECTED=$2
local VALUE=$(resetprop ${'$'}NAME)
[ -z ${'$'}VALUE ] && resetprop ${'$'}NAME ${'$'}EXPECTED
}
check_missing_match_prop() {
local NAME=$1
local EXPECTED=$2
local VALUE=$(resetprop ${'$'}NAME)
[ -z ${'$'}VALUE ] || [ ${'$'}VALUE = ${'$'}EXPECTED ] || resetprop ${'$'}NAME ${'$'}EXPECTED
[ -z ${'$'}VALUE ] && resetprop ${'$'}NAME ${'$'}EXPECTED
}
contains_reset_prop() {
local NAME=$1
local CONTAINS=$2
local NEWVAL=$3
[[ "$(resetprop ${'$'}NAME)" = *"${'$'}CONTAINS"* ]] && resetprop ${'$'}NAME ${'$'}NEWVAL
}
""".trimIndent())
appendLine()
appendLine("resetprop -w sys.boot_completed 0")
appendLine()
// 添加所有系统属性重置
val systemProps = listOf(
"ro.boot.vbmeta.invalidate_on_error" to "yes",
"ro.boot.vbmeta.avb_version" to "1.2",
"ro.boot.vbmeta.hash_alg" to "sha256",
"ro.boot.vbmeta.device_state" to "locked",
"ro.boot.verifiedbootstate" to "green",
"ro.boot.flash.locked" to "1",
"ro.boot.veritymode" to "enforcing",
"ro.boot.warranty_bit" to "0",
"ro.warranty_bit" to "0",
"ro.debuggable" to "0",
"ro.force.debuggable" to "0",
"ro.secure" to "1",
"ro.adb.secure" to "1",
"ro.build.type" to "user",
"ro.build.tags" to "release-keys",
"ro.vendor.boot.warranty_bit" to "0",
"ro.vendor.warranty_bit" to "0",
"vendor.boot.vbmeta.device_state" to "locked",
"vendor.boot.verifiedbootstate" to "green",
"sys.oem_unlock_allowed" to "0",
"ro.secureboot.lockstate" to "locked",
"ro.boot.realmebootstate" to "green",
"ro.boot.realme.lockstate" to "1",
"ro.crypto.state" to "encrypted"
)
systemProps.forEach { (prop, value) ->
when {
prop.startsWith("ro.boot.vbmeta") && prop.endsWith("_on_error") ->
appendLine("check_missing_prop \"$prop\" \"$value\"")
prop.contains("device_state") || prop.contains("verifiedbootstate") ->
appendLine("check_missing_match_prop \"$prop\" \"$value\"")
else ->
appendLine("check_reset_prop \"$prop\" \"$value\"")
}
}
appendLine()
appendLine("# Hide adb debugging traces")
appendLine("resetprop \"sys.usb.adb.disabled\" \" \"")
appendLine()
appendLine("# Hide recovery boot mode")
appendLine("contains_reset_prop \"ro.bootmode\" \"recovery\" \"unknown\"")
appendLine("contains_reset_prop \"ro.boot.bootmode\" \"recovery\" \"unknown\"")
appendLine("contains_reset_prop \"vendor.boot.bootmode\" \"recovery\" \"unknown\"")
appendLine()
appendLine("# Hide cloudphone detection")
appendLine("[ -n \"$(resetprop ro.kernel.qemu)\" ] && resetprop ro.kernel.qemu \"\"")
appendLine()
}
/**
* 生成post-fs-data.sh脚本内容
*/
fun generatePostFsDataScript(
targetPath: String,
unameValue: String,
buildTimeValue: String,
executeInPostFsData: Boolean = false
): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Post-FS-Data Script")
appendLine("# 在文件系统挂载后但在系统完全启动前执行")
appendLine()
appendLine(generateLogSetup("susfs_post_fs_data.log"))
appendLine()
appendLine(generateBinaryCheck(targetPath))
appendLine()
appendLine("echo \"$(get_current_time): Post-FS-Data脚本开始执行\" >> \"${'$'}LOG_FILE\"")
appendLine()
// 设置uname和构建时间 - 只有在选择在post-fs-data中执行时才执行
if (executeInPostFsData && (unameValue != DEFAULT_UNAME || buildTimeValue != DEFAULT_BUILD_TIME)) {
appendLine("# 设置uname和构建时间")
appendLine("\"${'$'}SUSFS_BIN\" set_uname '$unameValue' '$buildTimeValue'")
appendLine("echo \"$(get_current_time): 设置uname为: $unameValue, 构建时间为: $buildTimeValue\" >> \"${'$'}LOG_FILE\"")
appendLine()
}
appendLine("echo \"$(get_current_time): Post-FS-Data脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
/**
* 生成post-mount.sh脚本内容
*/
fun generatePostMountScript(
targetPath: String,
susMounts: Set<String>,
tryUmounts: Set<String>
): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Post-Mount Script")
appendLine("# 在所有分区挂载完成后执行")
appendLine()
appendLine(generateLogSetup("susfs_post_mount.log"))
appendLine()
appendLine("echo \"$(get_current_time): Post-Mount脚本开始执行\" >> \"${'$'}LOG_FILE\"")
appendLine()
appendLine(generateBinaryCheck(targetPath))
appendLine()
// 添加SUS挂载
if (susMounts.isNotEmpty()) {
appendLine("# 添加SUS挂载")
susMounts.forEach { mount ->
appendLine("\"${'$'}SUSFS_BIN\" add_sus_mount '$mount'")
appendLine("echo \"$(get_current_time): 添加SUS挂载: $mount\" >> \"${'$'}LOG_FILE\"")
}
appendLine()
}
// 添加尝试卸载
if (tryUmounts.isNotEmpty()) {
appendLine("# 添加尝试卸载")
tryUmounts.forEach { umount ->
val parts = umount.split("|")
if (parts.size == 2) {
val path = parts[0]
val mode = parts[1]
appendLine("\"${'$'}SUSFS_BIN\" add_try_umount '$path' $mode")
appendLine("echo \"$(get_current_time): 添加尝试卸载: $path (模式: $mode)\" >> \"${'$'}LOG_FILE\"")
}
}
appendLine()
}
appendLine("echo \"$(get_current_time): Post-Mount脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
/**
* 生成boot-completed.sh脚本内容
*/
fun generateBootCompletedScript(
targetPath: String,
hideSusMountsForAllProcs: Boolean = true
): String {
return buildString {
appendLine("#!/system/bin/sh")
appendLine("# SuSFS Boot-Completed Script")
appendLine("# 在系统完全启动后执行")
appendLine()
appendLine(generateLogSetup("susfs_boot_completed.log"))
appendLine()
appendLine("echo \"$(get_current_time): Boot-Completed脚本开始执行\" >> \"${'$'}LOG_FILE\"")
appendLine()
appendLine(generateBinaryCheck(targetPath))
appendLine()
// SUS挂载隐藏控制仅限1.5.8及以上版本
appendLine("# 设置SUS挂载隐藏控制仅限1.5.8及以上版本)")
appendLine("SUSFS_VERSION=$(${'$'}SUSFS_BIN show version 2>/dev/null | grep -o 'v[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+' | head -1)")
appendLine("if [ -n \"${'$'}SUSFS_VERSION\" ]; then")
appendLine(" VERSION_NUM=$(echo \"${'$'}SUSFS_VERSION\" | sed 's/v//' | awk -F. '{printf \"%d%02d%02d\", $1, $2, $3}')")
appendLine(" if [ \"${'$'}VERSION_NUM\" -ge 10508 ]; then")
val hideValue = if (hideSusMountsForAllProcs) 1 else 0
appendLine(" \"${'$'}SUSFS_BIN\" hide_sus_mnts_for_all_procs $hideValue")
appendLine(" echo \"$(get_current_time): SUS挂载隐藏控制设置为: ${if (hideSusMountsForAllProcs) "对所有进程隐藏" else "仅对非KSU进程隐藏"}\" >> \"${'$'}LOG_FILE\"")
appendLine(" else")
appendLine(" echo \"$(get_current_time): 当前版本 ${'$'}SUSFS_VERSION 不支持SUS挂载隐藏控制功能需要1.5.8及以上版本\" >> \"${'$'}LOG_FILE\"")
appendLine(" fi")
appendLine("else")
appendLine(" echo \"$(get_current_time): 无法获取SuSFS版本信息跳过SUS挂载隐藏控制设置\" >> \"${'$'}LOG_FILE\"")
appendLine("fi")
appendLine()
appendLine("echo \"$(get_current_time): Boot-Completed脚本执行完成\" >> \"${'$'}LOG_FILE\"")
}
}
/**
* 生成module.prop文件内容
*/
fun generateModuleProp(moduleId: String): String {
val moduleVersion = "v1.0.0"
val moduleVersionCode = "1000"
return """
id=$moduleId
name=SuSFS Manager
version=$moduleVersion
versionCode=$moduleVersionCode
author=ShirkNeko
description=SuSFS Manager Auto Configuration Module (自动生成请不要手动卸载或删除该模块! / Automatically generated Please do not manually uninstall or delete the module!)
updateJson=
""".trimIndent()
}
}

View File

@@ -0,0 +1,285 @@
package com.sukisu.ultra.ui.viewmodel
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.system.Os
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.dergoogler.mmrl.platform.Platform.Companion.context
import com.google.gson.Gson
import com.sukisu.ultra.KernelVersion
import com.sukisu.ultra.Natives
import com.sukisu.ultra.getKernelVersion
import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.util.module.LatestVersionInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import androidx.core.content.edit
class HomeViewModel : ViewModel() {
companion object {
private const val TAG = "HomeViewModel"
private const val PREFS_NAME = "home_cache"
private const val KEY_SYSTEM_STATUS = "system_status"
private const val KEY_SYSTEM_INFO = "system_info"
private const val KEY_VERSION_INFO = "version_info"
private const val KEY_LAST_UPDATE = "last_update_time"
}
// 系统状态
data class SystemStatus(
val isManager: Boolean = false,
val ksuVersion: Int? = null,
val lkmMode: Boolean? = null,
val kernelVersion: KernelVersion = getKernelVersion(),
val isRootAvailable: Boolean = false,
val isKpmConfigured: Boolean = false,
val requireNewKernel: Boolean = false
)
// 系统信息
data class SystemInfo(
val kernelRelease: String = "",
val androidVersion: String = "",
val deviceModel: String = "",
val managerVersion: Pair<String, Long> = Pair("", 0L),
val seLinuxStatus: String = "",
val kpmVersion: String = "",
val suSFSStatus: String = "",
val suSFSVersion: String = "",
val suSFSVariant: String = "",
val suSFSFeatures: String = "",
val susSUMode: String = "",
val superuserCount: Int = 0,
val moduleCount: Int = 0,
val kpmModuleCount: Int = 0
)
private val gson = Gson()
private val prefs by lazy { ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) }
var systemStatus by mutableStateOf(SystemStatus())
private set
var systemInfo by mutableStateOf(SystemInfo())
private set
var latestVersionInfo by mutableStateOf(LatestVersionInfo())
private set
var isSimpleMode by mutableStateOf(false)
private set
var isHideVersion by mutableStateOf(false)
private set
var isHideOtherInfo by mutableStateOf(false)
private set
var isHideSusfsStatus by mutableStateOf(false)
private set
var isHideLinkCard by mutableStateOf(false)
private set
var showKpmInfo by mutableStateOf(false)
private set
fun loadUserSettings(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
isSimpleMode = prefs.getBoolean("is_simple_mode", false)
isHideVersion = prefs.getBoolean("is_hide_version", false)
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false)
isHideSusfsStatus = prefs.getBoolean("is_hide_susfs_status", false)
isHideLinkCard = prefs.getBoolean("is_hide_link_card", false)
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
}
}
fun initializeData() {
viewModelScope.launch {
loadCachedData()
}
}
private fun loadCachedData() {
prefs.getString(KEY_SYSTEM_STATUS, null)?.let {
systemStatus = gson.fromJson(it, SystemStatus::class.java)
}
prefs.getString(KEY_SYSTEM_INFO, null)?.let {
systemInfo = gson.fromJson(it, SystemInfo::class.java)
}
prefs.getString(KEY_VERSION_INFO, null)?.let {
latestVersionInfo = gson.fromJson(it, LatestVersionInfo::class.java)
}
}
private suspend fun fetchAndSaveData() {
fetchSystemStatus()
fetchSystemInfo()
withContext(Dispatchers.IO) {
prefs.edit {
putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus))
putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo))
putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo))
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
}
}
}
fun checkForUpdates(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
try {
val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
.getBoolean("check_update", true)
if (checkUpdate) {
val newVersionInfo = checkNewVersion()
latestVersionInfo = newVersionInfo
prefs.edit {
putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo))
putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
}
}
} catch (e: Exception) {
Log.e(TAG, "Error checking for updates", e)
}
}
}
fun refreshAllData(context: Context) {
viewModelScope.launch {
try {
fetchAndSaveData()
checkForUpdates(context)
} catch (e: Exception) {
Log.e(TAG, "Error refreshing data", e)
}
}
}
private suspend fun fetchSystemStatus() {
withContext(Dispatchers.IO) {
try {
val kernelVersion = getKernelVersion()
val isManager = Natives.becomeManager(ksuApp.packageName)
val ksuVersion = if (isManager) Natives.version else null
val lkmMode = ksuVersion?.let {
if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null
}
systemStatus = SystemStatus(
isManager = isManager,
ksuVersion = ksuVersion,
lkmMode = lkmMode,
kernelVersion = kernelVersion,
isRootAvailable = rootAvailable(),
isKpmConfigured = Natives.isKPMEnabled(),
requireNewKernel = isManager && Natives.requireNewKernel()
)
} catch (e: Exception) {
Log.e(TAG, "Error fetching system status", e)
}
}
}
@SuppressLint("RestrictedApi")
private suspend fun fetchSystemInfo() {
withContext(Dispatchers.IO) {
try {
val uname = Os.uname()
val kpmVersion = getKpmVersion()
val suSFS = getSuSFS()
var suSFSVersion = ""
var suSFSVariant = ""
var suSFSFeatures = ""
var susSUMode = ""
if (suSFS == "Supported") {
suSFSVersion = getSuSFSVersion()
if (suSFSVersion.isNotEmpty()) {
suSFSVariant = getSuSFSVariant()
suSFSFeatures = getSuSFSFeatures()
val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU"
if (isSUS_SU) {
susSUMode = try {
susfsSUS_SU_Mode().toString()
} catch (_: Exception) {
""
}
}
}
}
systemInfo = SystemInfo(
kernelRelease = uname.release,
androidVersion = Build.VERSION.RELEASE,
deviceModel = getDeviceModel(),
managerVersion = getManagerVersion(ksuApp.applicationContext),
seLinuxStatus = getSELinuxStatus(context),
kpmVersion = kpmVersion,
suSFSStatus = suSFS,
suSFSVersion = suSFSVersion,
suSFSVariant = suSFSVariant,
suSFSFeatures = suSFSFeatures,
susSUMode = susSUMode,
superuserCount = getSuperuserCount(),
moduleCount = getModuleCount(),
kpmModuleCount = getKpmModuleCount()
)
} catch (e: Exception) {
Log.e(TAG, "Error fetching system info", e)
}
}
}
private fun getDeviceInfo(): String {
var manufacturer =
Build.MANUFACTURER[0].uppercaseChar().toString() + Build.MANUFACTURER.substring(1)
if (!Build.BRAND.equals(Build.MANUFACTURER, ignoreCase = true)) {
manufacturer += " " + Build.BRAND[0].uppercaseChar() + Build.BRAND.substring(1)
}
manufacturer += " " + Build.MODEL + " "
return manufacturer
}
@SuppressLint("PrivateApi")
private fun getDeviceModel(): String {
return try {
val systemProperties = Class.forName("android.os.SystemProperties")
val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java)
val marketNameKeys = listOf(
"ro.product.marketname", // Xiaomi
"ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme
"ro.vivo.market.name", // Vivo
"ro.config.marketing_name" // Huawei
)
var result = getDeviceInfo()
for (key in marketNameKeys) {
val marketName = getMethod.invoke(null, key, "") as String
if (marketName.isNotEmpty()) {
result = marketName
break
}
}
result
} catch (e: Exception) {
Log.e(TAG, "Error getting device model", e)
getDeviceInfo()
}
}
private fun getManagerVersion(context: Context): Pair<String, Long> {
return try {
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!
val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo)
Pair(packageInfo.versionName!!, versionCode)
} catch (e: Exception) {
Log.e(TAG, "Error getting manager version", e)
Pair("", 0L)
}
}
}

View File

@@ -11,6 +11,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.*
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
class KpmViewModel : ViewModel() { class KpmViewModel : ViewModel() {
var moduleList by mutableStateOf(emptyList<ModuleInfo>()) var moduleList by mutableStateOf(emptyList<ModuleInfo>())
private set private set

View File

@@ -1,5 +1,6 @@
package com.sukisu.ultra.ui.viewmodel package com.sukisu.ultra.ui.viewmodel
import android.content.Context
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
@@ -8,20 +9,67 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.dergoogler.mmrl.platform.model.ModuleConfig
import com.dergoogler.mmrl.platform.model.ModuleConfig.Companion.asModuleConfig
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.HanziToPinyin
import com.sukisu.ultra.ui.util.listModules import com.sukisu.ultra.ui.util.listModules
import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.Collator import java.text.Collator
import java.text.DecimalFormat
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit
import kotlin.math.log10
import kotlin.math.pow
import androidx.core.content.edit
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
class ModuleViewModel : ViewModel() { class ModuleViewModel : ViewModel() {
companion object { companion object {
private const val TAG = "ModuleViewModel" private const val TAG = "ModuleViewModel"
private var modules by mutableStateOf<List<ModuleInfo>>(emptyList()) private var modules by mutableStateOf<List<ModuleInfo>>(emptyList())
private const val CUSTOM_USER_AGENT = "SukiSU-Ultra/2.0"
}
// 模块大小缓存管理器
private lateinit var moduleSizeCache: ModuleSizeCache
fun initializeCache(context: Context) {
if (!::moduleSizeCache.isInitialized) {
moduleSizeCache = ModuleSizeCache(context)
}
}
fun getModuleSize(dirId: String): String {
if (!::moduleSizeCache.isInitialized) {
return "0 KB"
}
val size = moduleSizeCache.getModuleSize(dirId)
return formatFileSize(size)
}
/**
* 刷新所有模块的大小缓存
* 只在安装、卸载、更新模块后调用
*/
fun refreshModuleSizeCache() {
if (!::moduleSizeCache.isInitialized) return
viewModelScope.launch(Dispatchers.IO) {
Log.d(TAG, "开始刷新模块大小缓存")
val currentModules = modules.map { it.dirId }
moduleSizeCache.refreshCache(currentModules)
Log.d(TAG, "模块大小缓存刷新完成")
}
} }
class ModuleInfo( class ModuleInfo(
@@ -38,6 +86,7 @@ class ModuleViewModel : ViewModel() {
val hasWebUi: Boolean, val hasWebUi: Boolean,
val hasActionScript: Boolean, val hasActionScript: Boolean,
val dirId: String, // real module id (dir name) val dirId: String, // real module id (dir name)
var config: ModuleConfig? = null,
) )
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
@@ -65,6 +114,8 @@ class ModuleViewModel : ViewModel() {
fun markNeedRefresh() { fun markNeedRefresh() {
isNeedRefresh = true isNeedRefresh = true
// 标记需要刷新时,同时刷新大小缓存
refreshModuleSizeCache()
} }
fun fetchModuleList() { fun fetchModuleList() {
@@ -98,9 +149,49 @@ class ModuleViewModel : ViewModel() {
obj.optString("updateJson"), obj.optString("updateJson"),
obj.optBoolean("web"), obj.optBoolean("web"),
obj.optBoolean("action"), obj.optBoolean("action"),
obj.getString("dir_id"), obj.getString("dir_id")
) )
}.toList() }.toList()
launch {
modules.forEach { module ->
withContext(Dispatchers.IO) {
try {
runCatching {
module.config = module.id.asModuleConfig
}.onFailure { e ->
Log.e(TAG, "Failed to load config from id for module ${module.id}", e)
}
if (module.config == null) {
runCatching {
module.config = module.name.asModuleConfig
}.onFailure { e ->
Log.e(TAG, "Failed to load config from name for module ${module.id}", e)
}
}
if (module.config == null) {
runCatching {
module.config = module.description.asModuleConfig
}.onFailure { e ->
Log.e(TAG, "Failed to load config from description for module ${module.id}", e)
}
}
if (module.config == null) {
module.config = ModuleConfig()
}
} catch (e: Exception) {
Log.e(TAG, "Failed to load any config for module ${module.id}", e)
module.config = ModuleConfig()
}
}
}
}
// 首次加载模块列表时,初始化缓存
if (::moduleSizeCache.isInitialized) {
val currentModules = modules.map { it.dirId }
moduleSizeCache.initializeCacheIfNeeded(currentModules)
}
isNeedRefresh = false isNeedRefresh = false
}.onFailure { e -> }.onFailure { e ->
Log.e(TAG, "fetchModuleList: ", e) Log.e(TAG, "fetchModuleList: ", e)
@@ -117,6 +208,10 @@ class ModuleViewModel : ViewModel() {
} }
} }
private fun sanitizeVersionString(version: String): String {
return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_")
}
fun checkUpdate(m: ModuleInfo): Triple<String, String, String> { fun checkUpdate(m: ModuleInfo): Triple<String, String, String> {
val empty = Triple("", "", "") val empty = Triple("", "", "")
if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) { if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) {
@@ -126,19 +221,32 @@ class ModuleViewModel : ViewModel() {
val result = kotlin.runCatching { val result = kotlin.runCatching {
val url = m.updateJson val url = m.updateJson
Log.i(TAG, "checkUpdate url: $url") Log.i(TAG, "checkUpdate url: $url")
val response = okhttp3.OkHttpClient()
.newCall( val client = okhttp3.OkHttpClient.Builder()
okhttp3.Request.Builder() .connectTimeout(15, TimeUnit.SECONDS)
.url(url) .readTimeout(30, TimeUnit.SECONDS)
.build() .writeTimeout(15, TimeUnit.SECONDS)
).execute() .build()
val request = okhttp3.Request.Builder()
.url(url)
.header("User-Agent", CUSTOM_USER_AGENT)
.build()
val response = client.newCall(request).execute()
Log.d(TAG, "checkUpdate code: ${response.code}") Log.d(TAG, "checkUpdate code: ${response.code}")
if (response.isSuccessful) { if (response.isSuccessful) {
response.body?.string() ?: "" response.body?.string() ?: ""
} else { } else {
Log.d(TAG, "checkUpdate failed: ${response.message}")
"" ""
} }
}.getOrDefault("") }.getOrElse { e ->
Log.e(TAG, "checkUpdate exception", e)
""
}
Log.i(TAG, "checkUpdate result: $result") Log.i(TAG, "checkUpdate result: $result")
if (result.isEmpty()) { if (result.isEmpty()) {
@@ -149,7 +257,8 @@ class ModuleViewModel : ViewModel() {
JSONObject(result) JSONObject(result)
}.getOrNull() ?: return empty }.getOrNull() ?: return empty
val version = updateJson.optString("version", "") var version = updateJson.optString("version", "")
version = sanitizeVersionString(version)
val versionCode = updateJson.optInt("versionCode", 0) val versionCode = updateJson.optInt("versionCode", 0)
val zipUrl = updateJson.optString("zipUrl", "") val zipUrl = updateJson.optString("zipUrl", "")
val changelog = updateJson.optString("changelog", "") val changelog = updateJson.optString("changelog", "")
@@ -160,3 +269,171 @@ class ModuleViewModel : ViewModel() {
return Triple(zipUrl, version, changelog) return Triple(zipUrl, version, changelog)
} }
} }
/**
* 模块大小缓存管理器
*/
class ModuleSizeCache(context: Context) {
companion object {
private const val TAG = "ModuleSizeCache"
private const val CACHE_PREFS_NAME = "module_size_cache"
private const val CACHE_VERSION_KEY = "cache_version"
private const val CACHE_INITIALIZED_KEY = "cache_initialized"
private const val CURRENT_CACHE_VERSION = 1
}
private val cachePrefs = context.getSharedPreferences(CACHE_PREFS_NAME, Context.MODE_PRIVATE)
private val sizeCache = mutableMapOf<String, Long>()
init {
loadCacheFromPrefs()
}
/**
* 从SharedPreferences加载缓存
*/
private fun loadCacheFromPrefs() {
try {
val cacheVersion = cachePrefs.getInt(CACHE_VERSION_KEY, 0)
if (cacheVersion != CURRENT_CACHE_VERSION) {
Log.d(TAG, "缓存版本不匹配,清空缓存")
clearCache()
return
}
val allEntries = cachePrefs.all
for ((key, value) in allEntries) {
if (key != CACHE_VERSION_KEY && key != CACHE_INITIALIZED_KEY && value is Long) {
sizeCache[key] = value
}
}
Log.d(TAG, "从缓存加载了 ${sizeCache.size} 个模块大小数据")
} catch (e: Exception) {
Log.e(TAG, "加载缓存失败", e)
clearCache()
}
}
/**
* 保存缓存到SharedPreferences
*/
private fun saveCacheToPrefs() {
try {
cachePrefs.edit {
putInt(CACHE_VERSION_KEY, CURRENT_CACHE_VERSION)
putBoolean(CACHE_INITIALIZED_KEY, true)
for ((dirId, size) in sizeCache) {
putLong(dirId, size)
}
}
Log.d(TAG, "保存了 ${sizeCache.size} 个模块大小到缓存")
} catch (e: Exception) {
Log.e(TAG, "保存缓存失败", e)
}
}
/**
* 获取模块大小(从缓存)
*/
fun getModuleSize(dirId: String): Long {
return sizeCache[dirId] ?: 0L
}
/**
* 检查缓存是否已初始化,如果没有则初始化
*/
fun initializeCacheIfNeeded(currentModules: List<String>) {
val isInitialized = cachePrefs.getBoolean(CACHE_INITIALIZED_KEY, false)
if (!isInitialized || sizeCache.isEmpty()) {
Log.d(TAG, "首次初始化缓存,计算所有模块大小")
refreshCache(currentModules)
} else {
// 检查是否有新模块需要计算大小
val newModules = currentModules.filter { !sizeCache.containsKey(it) }
if (newModules.isNotEmpty()) {
Log.d(TAG, "发现 ${newModules.size} 个新模块,计算大小: $newModules")
for (dirId in newModules) {
val size = calculateModuleFolderSize(dirId)
sizeCache[dirId] = size
Log.d(TAG, "新模块 $dirId 大小: ${formatFileSize(size)}")
}
saveCacheToPrefs()
}
}
}
/**
* 刷新所有模块的大小缓存
*/
fun refreshCache(currentModules: List<String>) {
try {
// 清理不存在的模块缓存
val toRemove = sizeCache.keys.filter { it !in currentModules }
toRemove.forEach { sizeCache.remove(it) }
if (toRemove.isNotEmpty()) {
Log.d(TAG, "清理了 ${toRemove.size} 个不存在的模块缓存: $toRemove")
}
// 计算所有当前模块的大小
for (dirId in currentModules) {
val size = calculateModuleFolderSize(dirId)
sizeCache[dirId] = size
Log.d(TAG, "更新模块 $dirId 大小: ${formatFileSize(size)}")
}
// 保存到持久化存储
saveCacheToPrefs()
} catch (e: Exception) {
Log.e(TAG, "刷新缓存失败", e)
}
}
/**
* 清空所有缓存
*/
private fun clearCache() {
sizeCache.clear()
cachePrefs.edit { clear() }
Log.d(TAG, "清空所有缓存")
}
/**
* 实际计算模块文件夹大小
*/
private fun calculateModuleFolderSize(dirId: String): Long {
return try {
val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "du -sb /data/adb/modules/$dirId"))
val reader = BufferedReader(InputStreamReader(process.inputStream))
val output = reader.readLine()
process.waitFor()
reader.close()
if (output != null) {
val sizeStr = output.split("\t").firstOrNull()
sizeStr?.toLongOrNull() ?: 0L
} else {
0L
}
} catch (e: Exception) {
Log.e(TAG, "计算模块大小失败 $dirId: ${e.message}")
0L
}
}
}
/**
* 格式化文件大小的工具函数
*/
fun formatFileSize(bytes: Long): String {
if (bytes <= 0) return "0 KB"
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val digitGroups = (log10(bytes.toDouble()) / log10(1024.0)).toInt()
return DecimalFormat("#,##0.#").format(
bytes / 1024.0.pow(digitGroups.toDouble())
) + " " + units[digitGroups]
}

View File

@@ -1,5 +1,7 @@
package com.sukisu.ultra.ui.viewmodel package com.sukisu.ultra.ui.viewmodel
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.Parcelable import android.os.Parcelable
@@ -7,29 +9,83 @@ import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.HanziToPinyin
import java.text.Collator import java.text.Collator
import java.util.* import java.util.*
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.LinkedBlockingQueue
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS
import com.sukisu.ultra.ui.webui.packageManager import com.sukisu.ultra.ui.webui.getInstalledPackagesAll
import com.sukisu.ultra.ui.webui.userManager
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import androidx.core.content.edit
import kotlinx.coroutines.asCoroutineDispatcher
// 应用分类
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"),
CUSTOM(com.sukisu.ultra.R.string.category_custom_apps, "CUSTOM"),
DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT");
companion object {
fun fromPersistKey(key: String): AppCategory {
return entries.find { it.persistKey == key } ?: ALL
}
}
}
// 排序方式
enum class SortType(val displayNameRes: Int, val persistKey: String) {
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"),
INSTALL_TIME_NEW(com.sukisu.ultra.R.string.sort_install_time_new, "INSTALL_TIME_NEW"),
INSTALL_TIME_OLD(com.sukisu.ultra.R.string.sort_install_time_old, "INSTALL_TIME_OLD"),
SIZE_DESC(com.sukisu.ultra.R.string.sort_size_desc, "SIZE_DESC"),
SIZE_ASC(com.sukisu.ultra.R.string.sort_size_asc, "SIZE_ASC"),
USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ");
companion object {
fun fromPersistKey(key: String): SortType {
return entries.find { it.persistKey == key } ?: NAME_ASC
}
}
}
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
class SuperUserViewModel : ViewModel() { class SuperUserViewModel : ViewModel() {
val isPlatformAlive get() = Platform.isAlive val isPlatformAlive get() = Platform.isAlive
companion object { companion object {
private const val TAG = "SuperUserViewModel" private const val TAG = "SuperUserViewModel"
private var apps by mutableStateOf<List<AppInfo>>(emptyList()) var apps by mutableStateOf<List<AppInfo>>(emptyList())
private const val PREFS_NAME = "settings"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_SELECTED_CATEGORY = "selected_category"
private const val KEY_CURRENT_SORT_TYPE = "current_sort_type"
private const val CORE_POOL_SIZE = 4
private const val MAX_POOL_SIZE = 8
private const val KEEP_ALIVE_TIME = 60L
private const val BATCH_SIZE = 20
} }
@Parcelize @Parcelize
@@ -58,8 +114,35 @@ class SuperUserViewModel : ViewModel() {
} }
} }
private val appProcessingThreadPool = ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
LinkedBlockingQueue()
) { runnable ->
Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply {
isDaemon = true
priority = Thread.NORM_PRIORITY
}
}.asCoroutineDispatcher()
private val appListMutex = Mutex()
private val configChangeListeners = mutableSetOf<(String) -> Unit>()
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
var search by mutableStateOf("") var search by mutableStateOf("")
var showSystemApps by mutableStateOf(false)
var showSystemApps by mutableStateOf(loadShowSystemApps())
private set
var selectedCategory by mutableStateOf(loadSelectedCategory())
private set
var currentSortType by mutableStateOf(loadCurrentSortType())
private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
@@ -69,6 +152,89 @@ class SuperUserViewModel : ViewModel() {
var selectedApps by mutableStateOf<Set<String>>(emptySet()) var selectedApps by mutableStateOf<Set<String>>(emptySet())
internal set internal set
// 加载进度状态
var loadingProgress by mutableFloatStateOf(0f)
private set
var loadingMessage by mutableStateOf("")
private set
/**
* 从SharedPreferences加载显示系统应用设置
*/
private fun loadShowSystemApps(): Boolean {
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
}
/**
* 从SharedPreferences加载选择的应用分类
*/
private fun loadSelectedCategory(): AppCategory {
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey
return AppCategory.fromPersistKey(categoryKey)
}
/**
* 从SharedPreferences加载当前排序方式
*/
private fun loadCurrentSortType(): SortType {
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey
return SortType.fromPersistKey(sortKey)
}
/**
* 更新显示系统应用设置并保存到SharedPreferences
*/
fun updateShowSystemApps(newValue: Boolean) {
showSystemApps = newValue
saveShowSystemApps(newValue)
}
/**
* 更新选择的应用分类并保存到SharedPreferences
*/
fun updateSelectedCategory(newCategory: AppCategory) {
selectedCategory = newCategory
saveSelectedCategory(newCategory)
}
/**
* 更新当前排序方式并保存到SharedPreferences
*/
fun updateCurrentSortType(newSortType: SortType) {
currentSortType = newSortType
saveCurrentSortType(newSortType)
}
/**
* 保存显示系统应用设置到SharedPreferences
*/
private fun saveShowSystemApps(value: Boolean) {
prefs.edit {
putBoolean(KEY_SHOW_SYSTEM_APPS, value)
}
Log.d(TAG, "Saved show system apps: $value")
}
/**
* 保存选择的应用分类到SharedPreferences
*/
private fun saveSelectedCategory(category: AppCategory) {
prefs.edit {
putString(KEY_SELECTED_CATEGORY, category.persistKey)
}
Log.d(TAG, "Saved selected category: ${category.persistKey}")
}
/**
* 保存当前排序方式到SharedPreferences
*/
private fun saveCurrentSortType(sortType: SortType) {
prefs.edit {
putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey)
}
Log.d(TAG, "Saved current sort type: ${sortType.persistKey}")
}
private val sortedList by derivedStateOf { private val sortedList by derivedStateOf {
val comparator = compareBy<AppInfo> { val comparator = compareBy<AppInfo> {
when { when {
@@ -124,6 +290,43 @@ class SuperUserViewModel : ViewModel() {
val profile = Natives.getAppProfile(packageName, it.uid) val profile = Natives.getAppProfile(packageName, it.uid)
val updatedProfile = profile.copy(allowSu = allowSu) val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) { if (Natives.setAppProfile(updatedProfile)) {
updateAppProfileLocally(packageName, updatedProfile)
notifyConfigChange(packageName)
}
}
}
clearSelection()
showBatchActions = false
refreshAppConfigurations()
}
// 批量更新权限和umount模块设置
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
selectedApps.forEach { packageName ->
val app = apps.find { it.packageName == packageName }
app?.let {
val profile = Natives.getAppProfile(packageName, it.uid)
val updatedProfile = profile.copy(
allowSu = allowSu,
umountModules = umountModules ?: profile.umountModules,
nonRootUseDefault = false
)
if (Natives.setAppProfile(updatedProfile)) {
updateAppProfileLocally(packageName, updatedProfile)
notifyConfigChange(packageName)
}
}
}
clearSelection()
showBatchActions = false
refreshAppConfigurations()
}
// 更新本地应用配置
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
appListMutex.tryLock().let { locked ->
if (locked) {
try {
apps = apps.map { app -> apps = apps.map { app ->
if (app.packageName == packageName) { if (app.packageName == packageName) {
app.copy(profile = updatedProfile) app.copy(profile = updatedProfile)
@@ -131,17 +334,67 @@ class SuperUserViewModel : ViewModel() {
app app
} }
} }
} finally {
appListMutex.unlock()
} }
} }
} }
clearSelection()
showBatchActions = false // 批量操作完成后退出批量模式
fetchAppList() // 刷新列表以显示最新状态
} }
private fun notifyConfigChange(packageName: String) {
configChangeListeners.forEach { listener ->
try {
listener(packageName)
} catch (e: Exception) {
Log.e(TAG, "Error notifying config change for $packageName", e)
}
}
}
/**
* 刷新应用配置状态
*/
suspend fun refreshAppConfigurations() {
withContext(appProcessingThreadPool) {
supervisorScope {
val currentApps = apps.toList()
val batches = currentApps.chunked(BATCH_SIZE)
loadingProgress = 0f
val updatedApps = batches.mapIndexed { batchIndex, batch ->
async {
val batchResult = batch.map { app ->
try {
val updatedProfile = Natives.getAppProfile(app.packageName, app.uid)
app.copy(profile = updatedProfile)
} catch (e: Exception) {
Log.e(TAG, "Error refreshing profile for ${app.packageName}", e)
app
}
}
val progress = (batchIndex + 1).toFloat() / batches.size
loadingProgress = progress
batchResult
}
}.awaitAll().flatten()
appListMutex.withLock {
apps = updatedApps
}
loadingProgress = 1f
Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps")
}
}
}
suspend fun fetchAppList() { suspend fun fetchAppList() {
isRefreshing = true isRefreshing = true
loadingProgress = 0f
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
withTimeoutOrNull(TIMEOUT_MILLIS) { withTimeoutOrNull(TIMEOUT_MILLIS) {
@@ -152,26 +405,87 @@ class SuperUserViewModel : ViewModel() {
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime() val start = SystemClock.elapsedRealtime()
val userInfos = Platform.userManager.getUsers() try {
val packages = mutableListOf<PackageInfo>() val packages = Platform.getInstalledPackagesAll {
val packageManager = Platform.packageManager Log.e(TAG, "getInstalledPackagesAll:", it)
}
for (userInfo in userInfos) { loadingProgress = 0.3f
Log.i(TAG, "fetchAppList: ${userInfo.id}")
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id)) val filteredPackages = packages.filter { it.packageName != ksuApp.packageName }
withContext(appProcessingThreadPool) {
supervisorScope {
val batches = filteredPackages.chunked(BATCH_SIZE)
val processedApps = batches.mapIndexed { batchIndex, batch ->
async {
val batchResult = batch.mapNotNull { packageInfo ->
try {
val appInfo = packageInfo.applicationInfo!!
val uid = appInfo.uid
val labelDeferred = async {
appInfo.loadLabel(pm).toString()
}
val profileDeferred = async {
Natives.getAppProfile(packageInfo.packageName, uid)
}
val label = labelDeferred.await()
val profile = profileDeferred.await()
AppInfo(
label = label,
packageInfo = packageInfo,
profile = profile,
)
} catch (e: Exception) {
Log.e(
TAG,
"Error processing app ${packageInfo.packageName}",
e
)
null
}
}
val progress = 0.3f + (batchIndex + 1).toFloat() / batches.size * 0.6f
loadingProgress = progress
batchResult
}
}.awaitAll().flatten()
appListMutex.withLock {
apps = processedApps
}
loadingProgress = 1f
val elapsed = SystemClock.elapsedRealtime() - start
Log.i(TAG, "Loaded ${processedApps.size} apps in ${elapsed}ms using concurrent processing")
}
}
} catch (e: Exception) {
Log.e(TAG, "Error fetching app list", e)
} finally {
isRefreshing = false
loadingProgress = 0f
loadingMessage = ""
} }
}
apps = packages.map { }
val appInfo = it.applicationInfo /**
val uid = appInfo!!.uid * 清理资源
val profile = Natives.getAppProfile(it.packageName, uid) */
AppInfo( override fun onCleared() {
label = appInfo.loadLabel(pm).toString(), super.onCleared()
packageInfo = it, try {
profile = profile, appProcessingThreadPool.close()
) configChangeListeners.clear()
}.filter { it.packageName != ksuApp.packageName } } catch (e: Exception) {
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") Log.e(TAG, "Error cleaning up resources", e)
} }
} }
} }

View File

@@ -2,9 +2,8 @@ package com.sukisu.ultra.ui.webui
import android.content.ServiceConnection import android.content.ServiceConnection
import android.util.Log import android.util.Log
import android.content.pm.PackageInfo
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.hiddenApi.HiddenPackageManager
import com.dergoogler.mmrl.platform.hiddenApi.HiddenUserManager
import com.dergoogler.mmrl.platform.model.IProvider import com.dergoogler.mmrl.platform.model.IProvider
import com.dergoogler.mmrl.platform.model.PlatformIntent import com.dergoogler.mmrl.platform.model.PlatformIntent
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
@@ -57,6 +56,17 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) {
} }
} }
val Platform.Companion.packageManager get(): HiddenPackageManager = HiddenPackageManager(this.mService) fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
val Platform.Companion.userManager get(): HiddenUserManager = HiddenUserManager(this.mService) try {
val packages = mutableListOf<PackageInfo>()
val userInfos = userManager.getUsers()
for (userInfo in userInfos) {
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
}
packages
} catch (e: Exception) {
catch(e)
packageManager.getInstalledPackages(0, userManager.myUserId)
}

View File

@@ -82,7 +82,6 @@ class WebUIXActivity : ComponentActivity() {
if (isLoading) { if (isLoading) {
Loading() Loading()
return@KernelSUTheme return@KernelSUTheme
} }

View File

@@ -9,8 +9,8 @@ import android.webkit.JavascriptInterface
import android.widget.Toast import android.widget.Toast
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.WindowInsetsControllerCompat
import com.dergoogler.mmrl.webui.interfaces.WXInterface
import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.dergoogler.mmrl.webui.interfaces.WXOptions
import com.dergoogler.mmrl.webui.interfaces.WebUIInterface
import com.dergoogler.mmrl.webui.model.JavaScriptInterface import com.dergoogler.mmrl.webui.model.JavaScriptInterface
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
@@ -20,14 +20,13 @@ import com.sukisu.ultra.ui.util.listModules
import com.sukisu.ultra.ui.util.withNewRootShell import com.sukisu.ultra.ui.util.withNewRootShell
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import com.sukisu.ultra.ui.util.controlKpmModule import com.sukisu.ultra.ui.util.*
import com.sukisu.ultra.ui.util.listKpmModules
import java.io.File import java.io.File
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
class WebViewInterface( class WebViewInterface(
wxOptions: WXOptions, wxOptions: WXOptions,
) : WebUIInterface(wxOptions) { ) : WXInterface(wxOptions) {
override var name: String = "ksu" override var name: String = "ksu"
companion object { companion object {
@@ -187,7 +186,7 @@ class WebViewInterface(
@JavascriptInterface @JavascriptInterface
fun moduleInfo(): String { fun moduleInfo(): String {
val moduleInfos = JSONArray(listModules()) val moduleInfos = JSONArray(listModules())
var currentModuleInfo = JSONObject() val currentModuleInfo = JSONObject()
currentModuleInfo.put("moduleDir", modDir) currentModuleInfo.put("moduleDir", modDir)
val moduleId = File(modDir).getName() val moduleId = File(modDir).getName()
for (i in 0 until moduleInfos.length()) { for (i in 0 until moduleInfos.length()) {
@@ -197,7 +196,7 @@ class WebViewInterface(
continue continue
} }
var keys = currentInfo.keys() val keys = currentInfo.keys()
for (key in keys) { for (key in keys) {
currentModuleInfo.put(key, currentInfo.get(key)) currentModuleInfo.put(key, currentInfo.get(key))
} }
@@ -226,4 +225,4 @@ fun hideSystemUI(window: Window) =
} }
fun showSystemUI(window: Window) = fun showSystemUI(window: Window) =
WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,46 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
import androidx.lifecycle.LifecycleCoroutineScope
import com.sukisu.ultra.ui.MainActivity
import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
object DataRefreshUtils {
fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) {
scope.launch(Dispatchers.IO) {
while (isActive) {
DataRefreshManager.refreshData()
delay(5000)
}
}
}
fun startSettingsMonitorCoroutine(
scope: LifecycleCoroutineScope,
activity: MainActivity,
settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>
) {
scope.launch(Dispatchers.IO) {
while (isActive) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
settingsStateFlow.value = MainActivity.SettingsState(
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
)
delay(1000)
}
}
}
fun refreshData(scope: LifecycleCoroutineScope) {
scope.launch {
DataRefreshManager.refreshData()
}
}
}

View File

@@ -0,0 +1,24 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
object DisplayUtils {
fun applyCustomDpi(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val customDpi = prefs.getInt("app_dpi", 0)
if (customDpi > 0) {
try {
val resources = context.resources
val metrics = resources.displayMetrics
metrics.density = customDpi / 160f
@Suppress("DEPRECATION")
metrics.scaledDensity = customDpi / 160f
metrics.densityDpi = customDpi
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}

View File

@@ -0,0 +1,48 @@
package zako.zako.zako.zakoui.activity.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import java.util.Locale
object LocaleUtils {
@SuppressLint("ObsoleteSdkInt")
fun applyLanguageSetting(context: Context) {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val resources = context.resources
val config = Configuration(resources.configuration)
config.setLocale(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.createConfigurationContext(config)
} else {
@Suppress("DEPRECATION")
resources.updateConfiguration(config, resources.displayMetrics)
}
}
}
fun applyLocale(context: Context): Context {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val languageCode = prefs.getString("app_language", "") ?: ""
var newContext = context
if (languageCode.isNotEmpty()) {
val locale = Locale.forLanguageTag(languageCode)
Locale.setDefault(locale)
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
newContext = context.createConfigurationContext(config)
}
return newContext
}
}

View File

@@ -0,0 +1,19 @@
package zako.zako.zako.zakoui.activity.util
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.navigation.NavBackStackEntry
import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle
object NavigationUtils {
fun defaultTransitions() = object : NavHostAnimatedDestinationStyle() {
override val enterTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> EnterTransition
get() = { fadeIn(animationSpec = tween(340)) }
override val exitTransition: AnimatedContentTransitionScope<NavBackStackEntry>.() -> ExitTransition
get() = { fadeOut(animationSpec = tween(340)) }
}
}

View File

@@ -0,0 +1,96 @@
package zako.zako.zako.zakoui.activity.util
import android.content.Context
import android.database.ContentObserver
import android.os.Handler
import androidx.core.content.edit
import com.sukisu.ultra.ui.MainActivity
import com.sukisu.ultra.ui.theme.CardConfig
import com.sukisu.ultra.ui.theme.ThemeConfig
import kotlinx.coroutines.flow.MutableStateFlow
class ThemeChangeContentObserver(
handler: Handler,
private val onThemeChanged: () -> Unit
) : ContentObserver(handler) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
onThemeChanged()
}
}
object ThemeUtils {
fun initializeThemeSettings(activity: MainActivity, settingsStateFlow: MutableStateFlow<MainActivity.SettingsState>) {
val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE)
val isFirstRun = prefs.getBoolean("is_first_run", true)
settingsStateFlow.value = MainActivity.SettingsState(
isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false),
showKpmInfo = prefs.getBoolean("show_kpm_info", false)
)
if (isFirstRun) {
ThemeConfig.preventBackgroundRefresh = false
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", false)
}
prefs.edit { putBoolean("is_first_run", false) }
}
// 加载保存的背景设置
loadThemeMode()
loadThemeColors()
loadDynamicColorState()
CardConfig.load(activity.applicationContext)
}
fun registerThemeChangeObserver(activity: MainActivity): ThemeChangeContentObserver {
val contentObserver = ThemeChangeContentObserver(Handler(activity.mainLooper)) {
activity.runOnUiThread {
if (!ThemeConfig.preventBackgroundRefresh) {
ThemeConfig.backgroundImageLoaded = false
loadCustomBackground()
}
}
}
activity.contentResolver.registerContentObserver(
android.provider.Settings.System.getUriFor("ui_night_mode"),
false,
contentObserver
)
return contentObserver
}
fun unregisterThemeChangeObserver(activity: MainActivity, observer: ThemeChangeContentObserver) {
activity.contentResolver.unregisterContentObserver(observer)
}
fun onActivityPause(activity: MainActivity) {
CardConfig.save(activity.applicationContext)
activity.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit {
putBoolean("prevent_background_refresh", true)
}
ThemeConfig.preventBackgroundRefresh = true
}
fun onActivityResume() {
if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) {
loadCustomBackground()
}
}
private fun loadThemeMode() {
}
private fun loadThemeColors() {
}
private fun loadDynamicColorState() {
}
private fun loadCustomBackground() {
}
}

View File

@@ -1,4 +1,4 @@
package com.sukisu.ultra.flash package zako.zako.zako.zakoui.flash
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
@@ -27,6 +27,11 @@ import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
data class FlashState( data class FlashState(
val isFlashing: Boolean = false, val isFlashing: Boolean = false,
val isCompleted: Boolean = false, val isCompleted: Boolean = false,

View File

@@ -0,0 +1,418 @@
package zako.zako.zako.zakoui.screen
import android.net.Uri
import android.os.Environment
import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import zako.zako.zako.zakoui.flash.HorizonKernelState
import zako.zako.zako.zakoui.flash.HorizonKernelWorker
import com.sukisu.ultra.ui.component.KeyEventBlocker
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import com.sukisu.ultra.ui.theme.CardConfig
import zako.zako.zako.zakoui.flash.FlashState
import kotlinx.coroutines.delay
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
private object KernelFlashStateHolder {
var currentState: HorizonKernelState? = null
var currentUri: Uri? = null
var currentSlot: String? = null
var isFlashing = false
}
/**
* Kernel刷写界面
*/
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun KernelFlashScreen(
navigator: DestinationsNavigator,
kernelUri: Uri,
selectedSlot: String? = null
) {
val context = LocalContext.current
val scrollState = rememberScrollState()
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
val snackBarHost = LocalSnackbarHost.current
val scope = rememberCoroutineScope()
var logText by rememberSaveable { mutableStateOf("") }
var showFloatAction by rememberSaveable { mutableStateOf(false) }
val logContent = rememberSaveable { StringBuilder() }
val horizonKernelState = remember {
if (KernelFlashStateHolder.currentState != null &&
KernelFlashStateHolder.currentUri == kernelUri &&
KernelFlashStateHolder.currentSlot == selectedSlot) {
KernelFlashStateHolder.currentState!!
} else {
HorizonKernelState().also {
KernelFlashStateHolder.currentState = it
KernelFlashStateHolder.currentUri = kernelUri
KernelFlashStateHolder.currentSlot = selectedSlot
KernelFlashStateHolder.isFlashing = false
}
}
}
val flashState by horizonKernelState.state.collectAsState()
val logSavedString = stringResource(R.string.log_saved)
val onFlashComplete = {
showFloatAction = true
KernelFlashStateHolder.isFlashing = false
}
// 开始刷写
LaunchedEffect(Unit) {
if (!KernelFlashStateHolder.isFlashing && !flashState.isCompleted && flashState.error.isEmpty()) {
withContext(Dispatchers.IO) {
KernelFlashStateHolder.isFlashing = true
val worker = HorizonKernelWorker(
context = context,
state = horizonKernelState,
slot = selectedSlot
)
worker.uri = kernelUri
worker.setOnFlashCompleteListener(onFlashComplete)
worker.start()
// 监听日志更新
while (!flashState.isCompleted && flashState.error.isEmpty()) {
if (flashState.logs.isNotEmpty()) {
logText = flashState.logs.joinToString("\n")
logContent.clear()
logContent.append(logText)
}
delay(100)
}
if (flashState.error.isNotEmpty()) {
logText += "\n${flashState.error}\n"
logContent.append("\n${flashState.error}\n")
KernelFlashStateHolder.isFlashing = false
} else if (flashState.isCompleted) {
logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n"
logContent.append("\n${context.getString(R.string.horizon_flash_complete)}\n\n\n")
}
}
} else {
logText = flashState.logs.joinToString("\n")
if (flashState.error.isNotEmpty()) {
logText += "\n${flashState.error}\n"
} else if (flashState.isCompleted) {
logText += "\n${context.getString(R.string.horizon_flash_complete)}\n\n\n"
showFloatAction = true
}
}
}
val onBack: () -> Unit = {
if (!flashState.isFlashing || flashState.isCompleted || flashState.error.isNotEmpty()) {
// 清理全局状态
if (flashState.isCompleted || flashState.error.isNotEmpty()) {
KernelFlashStateHolder.currentState = null
KernelFlashStateHolder.currentUri = null
KernelFlashStateHolder.currentSlot = null
KernelFlashStateHolder.isFlashing = false
}
navigator.popBackStack()
}
}
BackHandler(enabled = true) {
onBack()
}
Scaffold(
topBar = {
TopBar(
flashState = flashState,
onBack = onBack,
onSave = {
scope.launch {
val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault())
val date = format.format(Date())
val file = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"KernelSU_kernel_flash_log_${date}.log"
)
file.writeText(logContent.toString())
snackBarHost.showSnackbar(logSavedString.format(file.absolutePath))
}
},
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
if (showFloatAction) {
ExtendedFloatingActionButton(
onClick = {
scope.launch {
withContext(Dispatchers.IO) {
reboot()
}
}
},
icon = {
Icon(
Icons.Filled.Refresh,
contentDescription = stringResource(id = R.string.reboot)
)
},
text = {
Text(text = stringResource(id = R.string.reboot))
},
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
expanded = true
)
}
},
snackbarHost = { SnackbarHost(hostState = snackBarHost) },
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
containerColor = MaterialTheme.colorScheme.background
) { innerPadding ->
KeyEventBlocker {
it.key == Key.VolumeDown || it.key == Key.VolumeUp
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
.nestedScroll(scrollBehavior.nestedScrollConnection),
) {
FlashProgressIndicator(flashState)
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.verticalScroll(scrollState)
) {
LaunchedEffect(logText) {
scrollState.animateScrollTo(scrollState.maxValue)
}
Text(
modifier = Modifier.padding(16.dp),
text = logText,
style = MaterialTheme.typography.bodyMedium,
fontFamily = FontFamily.Monospace,
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
@Composable
private fun FlashProgressIndicator(flashState: FlashState) {
val progressColor = when {
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
val progress = animateFloatAsState(
targetValue = flashState.progress,
label = "FlashProgress"
)
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(
text = when {
flashState.error.isNotEmpty() -> stringResource(R.string.flash_failed)
flashState.isCompleted -> stringResource(R.string.flash_success)
else -> stringResource(R.string.flashing)
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = progressColor
)
when {
flashState.error.isNotEmpty() -> {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.error
)
}
flashState.isCompleted -> {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.tertiary
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
if (flashState.currentStep.isNotEmpty()) {
Text(
text = flashState.currentStep,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
}
LinearProgressIndicator(
progress = { progress.value },
modifier = Modifier
.fillMaxWidth()
.height(8.dp),
color = progressColor,
trackColor = MaterialTheme.colorScheme.surfaceVariant
)
if (flashState.error.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Default.Error,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.size(16.dp)
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = flashState.error,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.error,
modifier = Modifier
.fillMaxWidth()
.background(
MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.3f),
shape = MaterialTheme.shapes.small
)
.padding(8.dp)
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TopBar(
flashState: FlashState,
onBack: () -> Unit,
onSave: () -> Unit = {},
scrollBehavior: TopAppBarScrollBehavior? = null
) {
val statusColor = when {
flashState.error.isNotEmpty() -> MaterialTheme.colorScheme.error
flashState.isCompleted -> MaterialTheme.colorScheme.tertiary
else -> MaterialTheme.colorScheme.primary
}
val colorScheme = MaterialTheme.colorScheme
val cardColor = if (CardConfig.isCustomBackgroundEnabled) {
colorScheme.surfaceContainerLow
} else {
colorScheme.background
}
val cardAlpha = CardConfig.cardAlpha
TopAppBar(
title = {
Text(
text = stringResource(
when {
flashState.error.isNotEmpty() -> R.string.flash_failed
flashState.isCompleted -> R.string.flash_success
else -> R.string.kernel_flashing
}
),
style = MaterialTheme.typography.titleLarge,
color = statusColor
)
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurface
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = cardColor.copy(alpha = cardAlpha),
scrolledContainerColor = cardColor.copy(alpha = cardAlpha)
),
actions = {
IconButton(onClick = onSave) {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = stringResource(id = R.string.save_log),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
scrollBehavior = scrollBehavior
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,21 +2,772 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="108dp"
android:height="108dp" android:height="108dp"
android:viewportWidth="108" android:viewportWidth="512"
android:viewportHeight="108"> android:viewportHeight="512">
<group android:scaleX="0.45"
android:scaleY="0.45"
android:translateX="140.8"
android:translateY="140.8">
<group <path
android:scaleX="0.135" android:fillColor="#fff9f6"
android:scaleY="0.135"> android:strokeColor="#4c4f59"
<path android:strokeWidth="6.5"
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z" android:strokeMiterLimit="1.3"
android:strokeWidth="18" android:strokeLineCap="round"
android:strokeColor="#1e110d" /> android:pathData="M164.239,424.641 L133.168,450.694 C133.168,450.694,127.975,468.87,159.134,479.627 C190.293,490.384,372.425,481.111,372.425,481.111 C372.425,481.111,424.728,492.981,453.29,444.017 C452.919,444.759,444.387,463.306,444.387,463.306 L445.871,482.595 C445.871,482.595,503.606,473.181,476.327,333.64 C480.524,324.197,484.72,318.427,484.72,318.427 L481.572,296.657 C481.572,296.657,487.23,256.322,489.455,251.128 C491.681,245.935,499.099,240,499.099,240 C499.099,240,496.317,219.598,492.422,213.849 C491.68,210.511,499.284,178.795,494.462,155.797 C487.414,143.927,489.64,143.927,475.544,130.202 C475.544,130.573,484.447,85.3181,474.802,56.7556 C471.834,56.7556,442.53,45.9983,377.986,80.4958 C377.615,81.6086,351.649,50.4496,224.416,64.5453 C223.674,64.5453,181.016,26.7093,157.647,24.1127 C156.534,23.7418,135.02,20.0323,132.423,73.0769 C132.423,74.1897,88.281,89.0274,76.4109,137.992 C64.5408,186.956,65.2827,218.857,65.2827,218.857 C65.2827,218.857,46.3778,246.52,55.4846,243.89 C60.8314,242.346,60.4605,267.45,60.4605,267.45 L58.2349,306.77 C58.2349,306.77,43.7682,342.009,43.3972,366.492 C43.0263,390.974,42.6553,400.619,48.2194,415.085 C53.7835,429.552,68.9921,461.824,90.8776,478.145 C99.7802,478.516,109.796,480,109.796,480 L113.505,459.227 C113.505,459.227,132.423,475.177,145.777,474.065 C159.131,472.952,145.035,473.323,145.035,473.323 L131.31,455.518 Z" />
<path <path
android:fillColor="#1e110d" android:fillColor="#93d4fa"
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" /> android:strokeColor="#4c4f59"
<path android:strokeWidth="6.9"
android:fillColor="#1e110d" android:strokeLineJoin="round"
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" /> android:strokeMiterLimit="1.3"
</group> android:strokeLineCap="round"
android:pathData="M133.91,110.17 L64.1717,157.65 L82.4357,188.404 L50.8182,244.079 L79.3807,261.142 L94.2186,259.658 L96.4452,204.017 L99.023,199.626 L117.217,177.681 L140.957,127.974 Z" />
<path
android:fillColor="#ace0fe"
android:strokeColor="#4c4f59"
android:strokeWidth="6.54357"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M217.014,405.828 C217.014,405.828,224.431,379.883,258.18,378.755 C291.929,377.627,311.955,382.891,311.955,382.891 C311.955,382.891,323.081,389.283,329.756,397.555 C336.432,405.827,358.683,438.164,371.293,446.436 C383.902,454.708,386.498,461.852,386.498,461.852 L384.644,473.132 C384.644,473.132,369.809,483.284,353.862,484.788 C337.915,486.292,206.258,484.788,206.258,484.788 L176.218,481.78" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.2"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M363.541,444.682 C363.541,444.682,356.647,448.748,358.68,451.488 C360.713,454.228,364.602,454.935,364.602,454.935 C364.602,454.935,369.198,455.2,369.729,451.399 C370.259,447.598,366.724,444.593,366.724,444.593" />
<path
android:fillColor="#ffffff"
android:strokeWidth="1.2"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M349.677,459.826 C349.677,459.826,354.177,452.576,356.677,454.326 C359.177,456.076,352.427,461.576,352.427,461.576" />
<path
android:fillColor="#ffffff"
android:strokeWidth="8.7"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M228.97,465.109 A8.0341015,5.6995392,0,0,1,220.936,470.809 A8.0341015,5.6995392,0,0,1,212.902,465.109 A8.0341015,5.6995392,0,0,1,220.936,459.409 A8.0341015,5.6995392,0,0,1,228.97,465.109 Z" />
<path
android:strokeColor="#6c9cb2"
android:strokeWidth="3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M220.852,408.918 C220.625,409.655,221.622,410.397,221.054,409.289 C220.495,408.199,221.513,410.375,221.635,410.685 C223.736,416.022,223.599,421.847,225.242,427.328 C225.544,428.335,225.83,429.367,225.836,430.427" />
<path
android:strokeColor="#6c9cb2"
android:strokeWidth="6.9"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M225.574,436.984 C224.352,441.357,225.651,446.212,227.992,449.836 C230.334,453.462,228.851,458.145,230.558,461.902 C232.274,465.678,232.769,470.295,230.257,474.062 C228.277,477.032,225.018,479.198,223.87,482.677 C223.87,482.677,223.796,483.338,223.796,483.338" />
<path
android:fillColor="#fbf3ef"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M259.148,381.902 C259.148,381.902,268.066,397.377,268.328,402.886 C268.59,408.394,300.59,401.05,300.59,401.05 C300.59,401.05,296.131,384.263,293.246,381.64 C290.361,379.017,259.148,381.902,259.148,381.902 Z" />
<path
android:strokeColor="#6c9cb2"
android:strokeWidth="4.6"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M247.869,386.361 C248.493,387.123,249.181,387.683,248.222,386.467 C247.237,385.218,247.587,385.904,248.316,386.709 C249.718,388.257,250.625,390.115,251.419,392.051 C252.907,395.677,253.575,399.582,255.44,403.076 C256.689,405.416,258.189,407.614,259.411,409.968" />
<path
android:strokeColor="#6c9cb2"
android:strokeWidth="4.6"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M306.623,386.885 C308.253,392.964,310.665,398.821,313.443,404.459" />
<path
android:strokeColor="#6c9cb2"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M335.738,431.213 C336.508,434.977,338.987,438.481,341.225,442.105 C343.323,445.501,344.561,449.237,346.104,452.901 C348.174,457.813,352.855,461.643,353.803,466.855 C354.723,471.917,359.045,475.752,359.082,481.05" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M258.623,384.262 C258.623,384.262,266.754,391.082,269.902,411.278" />
<path
android:fillColor="#fbf3ef"
android:strokeColor="#4c4f59"
android:strokeWidth="6.9"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M127.233,329.025 C127.233,329.025,124.636,389.488,140.958,404.326 C149.119,396.165,150.973,393.94,150.973,393.94 L163.214,402.472 L168.407,398.021 L170.633,378.361 L175.826,368.346 L181.761,356.105 L173.229,347.573 L157.279,328.284 L153.941,323.462 L149.49,332.736 C149.49,332.736,137.249,332.736,133.54,330.139 C129.831,327.542,127.234,329.026,127.234,329.026 Z" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M291.672,381.377 C291.672,381.377,298.754,387.934,301.902,407.869" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M270.164,404.197 C270.164,404.197,286.164,400.263,297.967,401.574" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M253.902,417.836 C253.902,417.836,291.148,404.721,323.672,408.656 C325.77,412.328,329.442,425.443,317.902,429.64 C313.181,429.64,298.754,428.066,295.345,429.115 C291.935,430.164,282.755,429.64,276.984,433.574 C271.214,437.508,262.295,442.492,258.623,436.722 C254.951,430.952,253.902,424.394,253.902,424.394 Z" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M276.459,482.623 C276.459,482.623,266.754,481.049,277.508,436.197" />
<path
android:fillColor="#fcf6fa"
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M287.213,432.787 L293.508,483.672 L307.41,482.885 L300.59,428.852 Z" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M287.738,430.951 L293.508,481.836" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M300.328,428.066 C300.328,428.066,302.426,465.837,307.672,481.05" />
<path
android:strokeColor="#494d55"
android:strokeWidth="5.3"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M313.18,431.213 C313.18,431.213,320.787,458.754,323.41,467.147 C326.033,475.54,315.279,482.885,315.279,482.885" />
<path
android:fillColor="#bce4fd"
android:strokeWidth="8.7"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M164.239,424.641 L133.168,450.694 L138.176,464.419 L159.134,479.628 L176.219,481.782 L165.09,461.23 L163.544,438.446 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="8.7"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M163.818,451.101 A8.0341015,5.6995392,0,0,1,155.784,456.801 A8.0341015,5.6995392,0,0,1,147.75,451.101 A8.0341015,5.6995392,0,0,1,155.784,445.401 A8.0341015,5.6995392,0,0,1,163.818,451.101 Z" />
<path
android:fillColor="#fff9f6"
android:strokeWidth="8.7"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M338.361,376.131 C338.361,376.131,340.197,407.082,332.066,427.541 C334.951,428.59,346.547,427.807,358.979,420.199 C371.411,412.591,370.361,398.164,370.361,398.164 L372.459,427.016 C372.459,427.016,396.778,398.917,396.684,374.409 C396.59,349.901,395.016,348.327,395.016,348.327 L383.182,347.2 Z" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="2.845"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M339.782,389.859 C339.782,389.859,377.989,382.069,396.165,363.893 C401.729,372.425,404.326,396.536,404.326,396.536 L406.923,409.519 L423.244,346.83 L416.567,341.637 L410.632,324.203 C410.632,324.203,395.423,339.412,383.182,347.201 C370.941,354.991,349.055,366.49,349.055,366.49 L338.298,370.199 Z" />
<path
android:strokeColor="#c3b4b0"
android:strokeWidth="2.845"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M347.016,369.643 C347.016,369.643,349.242,379.658,345.161,389.488 C341.081,399.318,340.153,398.947,340.153,398.947" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="6.945"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M380.852,480 C380.852,480,425.442,462.689,440.131,392.918 C446.951,390.295,474.229,378.229,479.475,367.213 C482.098,357.246,475.803,335.213,475.803,335.213 L451.147,335.738 L424.393,340.459 L419.672,352 L411.279,404.459 L398.164,432.787 L382.426,468.984 Z" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="8.74203"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M147.656,277.034 C147.656,277.034,136.529,381.17,275.24,380.421 C413.951,379.672,425.078,280.031,425.078,280.031" />
<path
android:fillColor="#fff9f6"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M123.279,147.934 L428.066,169.442 L403.41,129.049 C403.41,129.049,447.476,70.2949,454.295,68.7211 C461.115,67.1473,470.033,58.7539,470.033,58.7539 L456.918,55.0818 L379.803,77.1146 C379.803,77.1146,257.049,43.5408,211.41,70.8195 C165.771,98.0982,141.639,116.983,141.639,116.983 Z" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M331.445,66.6518 L347.674,97.4945 L395.504,128.862 L401.81,129.94 L428.693,94.6046 L396.59,72.3936 L377.705,77.6395 L347.279,66.0985 Z" />
<path
android:fillColor="#ace0fe"
android:strokeWidth="14.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M431.034,175.826 L448.097,158.763 L498.545,230.726 L492.61,248.531 L459.225,267.078 L446.613,250.757 L464.418,239.629 Z" />
<path
android:fillColor="#ace0fe"
android:strokeColor="#4c4f59"
android:strokeWidth="6.945"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M465.902,261.885 C465.902,261.885,501.512,316.413,468.87,335.331 C443.646,339.411,420.648,337.557,420.648,337.557 L431.034,311.22 L452.549,269.304 Z" />
<path
android:fillColor="#febdc7"
android:strokeWidth="4.6"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M471.869,57.7049 C471.869,57.7049,488.918,126.689,460.853,168.131 C453.509,168.393,451.673,157.639,451.673,157.639 L453.771,151.082 L452.722,141.902 L455.083,131.148 L456.919,125.64 L450.624,119.607 L444.067,114.361 L441.182,109.115 L444.067,103.869 L441.444,98.6231 L437.247,95.7379 L434.099,94.1641 L439.345,82.6231 L451.148,75.2788 Z" />
<path
android:fillColor="#ff1c1c"
android:fillAlpha="0.0509804"
android:strokeWidth="3.92397"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M244.267,326.814 A35.587568,21.480417,0,0,1,208.679,348.294 A35.587568,21.480417,0,0,1,173.091,326.814 A35.587568,21.480417,0,0,1,208.679,305.334 A35.587568,21.480417,0,0,1,244.267,326.814 Z" />
<path
android:fillColor="#ff1c1c"
android:fillAlpha="0.05044398"
android:strokeWidth="3.65169"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M355.204,336.752 A31.611719,20.942554,0,0,1,323.592,357.695 A31.611719,20.942554,0,0,1,291.98,336.752 A31.611719,20.942554,0,0,1,323.592,315.809 A31.611719,20.942554,0,0,1,355.204,336.752 Z" />
<path
android:fillColor="#fbf3ef"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M188.328,343.607 C188.328,343.607,200.394,315.279,204.066,349.902 C209.312,340.984,214.033,332.591,221.377,341.509 C228.197,343.607,236.59,348.853,226.098,363.542 C215.606,378.231,242.436,345.171,241.543,361.909 C240.822,375.421,217.021,408.72,217.021,408.72 C217.021,408.72,210.683,472.905,183.606,478.951 C147.058,487.112,171.54,375.082,171.54,375.082 Z" />
<path
android:fillColor="#fbe7e5"
android:fillAlpha="0.40581462"
android:strokeWidth="10.4"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M193.631,383.924 A12.612,12.241061,0,0,1,181.019,396.165 A12.612,12.241061,0,0,1,168.407,383.924 A12.612,12.241061,0,0,1,181.019,371.683 A12.612,12.241061,0,0,1,193.631,383.924 Z" />
<path
android:fillColor="#fee4e0"
android:fillAlpha="0.42411327"
android:strokeWidth="10.4"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M179.097,356.526 L193.193,335.753 L200.241,337.237 L200.983,350.962 L212.482,339.463 L221.756,343.543 L230.288,349.478 L224.724,364.316 L238.449,359.494 L235.111,377.299 L187.26,349.849 Z" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M188.328,343.607 C188.328,343.607,200.394,315.279,204.066,349.902 C209.312,340.984,214.033,332.591,221.377,341.509 C228.197,343.607,236.59,348.853,226.098,363.542 C215.606,378.231,242.436,345.171,241.543,361.909 C240.822,375.421,217.021,408.72,217.021,408.72 C217.021,408.72,210.683,472.905,183.606,478.951 C147.058,487.112,171.54,375.082,171.54,375.082 Z" />
<path
android:fillColor="#fff9f6"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M137.443,123.279 C137.443,123.279,186.782,74.6292,224.42,64.5441 C224.525,65.3118,104.253,-57.6139,137.443,123.279 Z" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="1.18016"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M198.852,42 L135.218,81.0166 L139.059,121.956 L184.828,87 L224,64.5 Z" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0.465732"
android:strokeColor="#a18f90"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M404.964,131.773 C405.489,131.004,406,130.224,406.539,129.465 C409.363,125.492,412.359,121.645,415.217,117.696 C417.61,114.388,419.157,112.153,421.508,108.796 C426.834,101.216,432.688,94.0352,438.717,87.009 C441.739,83.4584,444.784,79.9101,448.194,76.7199 C450.156,74.8844,452.413,73.0471,454.468,71.3278 C459.576,66.9703,465.137,63.2182,470.69,59.4657 C470.69,59.4657,467.765,57.6015,467.765,57.6015 L467.765,57.6015 C462.349,61.5016,456.862,65.3066,451.773,69.6416 C449.774,71.287,447.329,73.2528,445.41,75.0108 C441.958,78.1737,438.921,81.7424,435.922,85.3274 C429.967,92.4153,424.289,99.7266,418.922,107.272 C413.451,115.001,407.959,122.727,401.809,129.941 Z" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0.465732"
android:strokeColor="#a18f90"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M431.705,96.8851 C432.388,96.8683,433.075,96.8931,433.759,96.9175 C435.025,96.9602,436.284,97.0933,437.535,97.2918 C438.884,97.5112,440.212,97.8397,441.533,98.1893 C442.837,98.5317,444.12,98.9495,445.392,99.3955 C445.74,99.5276,446.281,99.7279,446.624,99.8789 C446.813,99.9624,447.362,100.253,447.184,100.147 C442.264,97.2348,444.346,98.2776,445.168,99.2354 C445.635,100.178,444.914,101.046,444.335,101.751 C443.282,102.866,441.955,103.674,440.684,104.516 C439.979,105.042,438.841,105.512,438.657,106.495 C438.521,107.219,438.804,107.502,439.115,108.14 C439.373,108.455,439.611,108.787,439.889,109.085 C440.52,109.761,441.438,110.538,442.141,111.121 C443.425,112.185,444.754,113.196,446.067,114.223 C448.295,115.955,450.534,117.67,452.508,119.693 C453.753,121.058,454.844,122.554,455.616,124.235 C456.19,125.547,456.235,126.917,456.081,128.315 C455.929,129.857,455.069,131.063,454.111,132.22 C452.86,133.613,451.408,134.8,450.128,136.163 C449.51,136.928,448.934,137.773,448.792,138.77 C448.716,139.301,448.777,139.643,448.823,140.171 C449.005,141.517,449.421,142.813,449.857,144.095 C450.319,145.456,450.958,146.746,451.568,148.044 C452.147,149.17,452.449,150.372,452.547,151.627 C452.576,152.64,452.406,153.643,452.197,154.63 C452.014,155.489,451.806,156.25,451.273,156.96 C450.579,157.686,449.748,158.26,448.962,158.879 C448.962,158.879,452.094,160.848,452.094,160.848 L452.094,160.848 C452.901,160.205,453.758,159.61,454.468,158.856 C455.052,158.078,455.277,157.331,455.473,156.387 C455.691,155.367,455.861,154.331,455.857,153.286 C455.781,151.991,455.511,150.736,454.918,149.57 C454.311,148.282,453.67,147.005,453.196,145.659 C452.758,144.401,452.34,143.13,452.125,141.811 C452.072,141.379,451.995,140.958,452.039,140.519 C452.134,139.578,452.689,138.785,453.257,138.067 C454.534,136.692,455.986,135.496,457.243,134.102 C458.25,132.898,459.168,131.637,459.347,130.028 C459.522,128.576,459.514,127.143,458.951,125.764 C458.207,124.025,457.101,122.494,455.846,121.083 C455.078,120.279,454.934,120.098,454.085,119.342 C452.574,117.998,450.931,116.813,449.361,115.541 C448.052,114.517,446.728,113.509,445.442,112.456 C444.732,111.875,443.869,111.153,443.218,110.494 C442.948,110.22,442.706,109.919,442.45,109.632 C442.2,109.235,441.825,108.834,441.875,108.312 C441.954,107.485,443.212,106.929,443.774,106.494 C445.073,105.627,446.429,104.795,447.505,103.65 C448.192,102.813,448.909,101.854,448.58,100.714 C448.498,100.578,448.435,100.429,448.335,100.306 C448.225,100.172,448.104,100.04,447.956,99.9498 C446.131,98.8402,444.47,97.783,442.482,97.127 C441.204,96.6881,439.915,96.2798,438.606,95.9437 C437.278,95.6019,435.942,95.2832,434.585,95.0766 C433.319,94.8915,432.044,94.7705,430.765,94.7225 C430.077,94.6977,429.373,94.7137,428.694,94.6018 Z" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0.465732"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M472.565,132.892 C472.58,133.665,472.464,134.438,472.369,135.204 C472.142,136.869,471.719,138.496,471.276,140.113 C470.664,142.278,469.96,144.416,469.202,146.534 C468.343,148.936,467.316,151.271,466.158,153.543 C465.033,155.731,463.751,157.831,462.385,159.876 C461.185,161.66,459.873,163.363,458.525,165.037 C457.749,165.984,456.962,166.921,456.176,167.859 C456.176,167.859,459.27,169.717,459.27,169.717 L459.27,169.717 C460.051,168.773,460.833,167.83,461.601,166.875 C462.941,165.183,464.252,163.468,465.446,161.67 C466.811,159.608,468.088,157.491,469.22,155.29 C470.384,153.005,471.417,150.659,472.298,148.249 C473.071,146.13,473.791,143.992,474.423,141.827 C474.89,140.207,475.334,138.577,475.608,136.911 C475.723,136.144,475.823,135.375,475.929,134.607 Z" />
<path
android:fillColor="#fff9f6"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M322.623,86.0328 C322.623,86.0328,398.056,112.153,432.787,175.213 C466.23,235.934,465.836,237.639,465.836,237.639 C465.836,237.639,466.361,246.032,445.902,249.18 C443.279,249.18,460.591,280.131,465.836,286.426 C451.147,293.77,449.049,295.344,449.049,295.344 C449.049,295.344,455.344,303.213,453.77,311.082 C436.459,310.557,431.213,310.033,431.213,310.033 C431.213,310.033,427.016,327.344,415.475,343.082 C410.23,333.115,410.23,320,410.23,320 C410.23,320,386.098,356.721,337.836,361.443 C336.262,358.82,350.426,347.279,355.672,333.115 C344.131,334.689,340.983,335.213,340.983,335.213 L351.475,318.426 C351.475,318.426,362.491,294.295,360.918,290.098 C359.869,287.475,328.918,232.393,328.918,232.393 C328.918,232.393,304.262,265.442,285.377,282.229 C278.557,278.032,271.738,267.54,271.738,267.54 L259.148,279.081 C259.148,279.081,210.71,228.317,217.054,188.898 C213.906,200.964,217.181,245.507,211.411,251.278 C206.165,249.18,195.149,236.589,197.247,210.36 C196.722,209.835,167.345,251.803,167.345,251.803 C167.345,251.803,163.148,300.59,183.607,325.246 C185.181,327.869,182.033,341.508,152.132,322.623 C153.181,321.049,151.083,332.59,151.083,332.59 C151.083,332.59,104.394,334.688,105.444,271.738 C102.821,269.115,91.8047,305.836,91.8047,305.836 C91.8047,305.836,67.6735,273.836,91.2801,212.984 C103.083,182.558,114.624,160.132,123.215,145.312 C131.805,130.492,137.445,123.279,137.445,123.279" />
<path
android:fillColor="#fffdfc"
android:strokeWidth="7.4278"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M355.672,146.885 A91.803276,41.967213,0,0,1,263.869,188.852 A91.803276,41.967213,0,0,1,172.066,146.885 A91.803276,41.967213,0,0,1,263.869,104.918 A91.803276,41.967213,0,0,1,355.672,146.885 Z" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M349.377,329.443 C349.377,329.443,420.721,287.476,428.066,238.164 C428.066,239.213,454.296,266.492,454.296,266.492 L465.837,290.623 L450.099,297.967 L452.197,311.606 L430.164,310.557 L417.574,340.983 L404.984,323.147 L381.902,349.377 L336.787,364.066 Z" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M325.246,144.787 C325.246,144.787,335.738,188.853,327.344,223.476 C327.344,233.968,360.918,284.328,360.918,284.328 C360.918,284.328,375.607,212.984,335.738,152.131 C327.345,146.885,325.246,144.787,325.246,144.787 Z" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M186.754,166.82 L209.836,167.869 C209.836,167.869,195.147,194.099,196.197,214.033 C194.099,210.885,174.164,245.508,174.164,245.508 C174.164,245.508,164.721,230.819,171.016,195.147 C180.459,178.36,186.754,166.819,186.754,166.819 Z" />
<path
android:fillColor="#fffdfc"
android:strokeWidth="6.94619"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M340.28,112.237 A80.289719,41.967213,0,0,1,259.99,154.204 A80.289719,41.967213,0,0,1,179.7,112.237 A80.289719,41.967213,0,0,1,259.99,70.2698 A80.289719,41.967213,0,0,1,340.28,112.237 Z" />
<path
android:strokeColor="#a18f90"
android:strokeWidth="7.7"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M138.623,122.492 C138.623,122.492,192.262,69.5084,231.607,64.2625" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M436.459,268.066 C436.459,268.066,440.131,286.427,433.836,310.558" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M418.098,305.836 L409.18,323.672" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M377.705,300.59 C377.705,300.59,380.328,312.656,355.672,332.59" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="4.2"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M284.328,97.5738 C284.328,97.5738,296.394,136.918,256.525,216.656 C257.05,216.656,310.033,177.836,312.656,127.476" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="5.1"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M272.787,87.082 C272.787,87.082,286.426,78.6886,291.672,86.5574 C308.845,97.3968,304.705,116.794,315.869,130.951 C317.662,132.804,323.041,130.17,323.672,132.721 C328.553,152.446,338.488,183.527,327.996,232.839" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="4.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M171.004,241.854 C171.004,241.854,142.442,181.762,263.368,94.219 C265.594,94.9609,204.017,148.377,197.34,205.502" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#c3b4b0"
android:strokeWidth="3"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M327.344,143.738 C327.344,143.738,352.524,164.197,359.344,209.312" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M362.492,268.59 L359.344,288.524" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M421.77,344.131 C421.77,344.131,419.147,402.361,388.196,455.869 C357.245,509.377,426.491,471.607,426.491,471.607" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M395.016,348.328 L407.082,408.656" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M371.934,423.869 C371.934,423.869,402.885,398.164,396.59,349.902" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M332.066,427.541 C332.066,427.541,368.787,429.115,370.886,398.164" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M372.459,427.016 L370.361,398.164" />
<path
android:fillColor="#93cefc"
android:strokeColor="#4d4e59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M377.509,228.61 C377.509,228.61,401.64,230.184,401.64,239.102 C401.115,242.774,390.099,256.938,378.033,257.987 C375.935,256.413,377.508,228.61,377.508,228.61 Z" />
<path
android:fillColor="#fffdfe"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M350.951,235.541 C350.951,235.541,344.394,213.508,352,213.508 C359.607,213.508,365.377,227.41,365.377,227.41 C365.377,227.41,370.436,209.683,375.344,212.197 C387.142,218.241,378.754,233.967,378.754,233.967 C378.754,233.967,397.377,259.934,366.688,261.246 C336.521,262.535,350.95,235.541,350.95,235.541 Z" />
<path
android:fillColor="#1a1a1a"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M332.066,425.967 C332.066,425.967,342.558,400.787,338.361,376.131" />
<path
android:fillColor="#e85240"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M192.262,268.852 C192.262,268.852,176.787,290.885,189.639,304.262 C232.393,304.524,232.918,304.787,232.918,304.787 C232.918,304.787,244.197,296.394,238.426,272.787 C212.983,262.82,192.262,268.853,192.262,268.853 Z" />
<path
android:fillColor="#fbb579"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M188.066,301.115 C187.624,292.333,201.519,287.099,210.361,286.689 C219.033,286.287,233.601,290.074,233.443,298.755 C233.366,303.017,221.902,304.263,221.902,304.263 C221.902,304.263,188.635,312.429,188.066,301.115 Z" />
<path
android:fillColor="#ffffff"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M217.967,273.049 A7.8688526,7.0819674,0,0,1,210.098,280.131 A7.8688526,7.0819674,0,0,1,202.229,273.049 A7.8688526,7.0819674,0,0,1,210.098,265.967 A7.8688526,7.0819674,0,0,1,217.967,273.049 Z" />
<path
android:strokeColor="#ebc2bf"
android:strokeWidth="2.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M174.164,245.508 C189.25,240.742,205.703,238.055,221.258,241.993 C224.556,242.828,227.776,243.986,230.82,245.508" />
<path
android:fillColor="#fde9e7"
android:strokeWidth="6.945"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M477.772,132.797 C477.772,132.797,491.126,142.812,492.61,150.231 C494.094,157.65,494.094,207.727,494.094,207.727 L489.643,208.84 L458.855,171.375 C458.855,171.375,467.758,156.908,469.983,148.748 C472.209,140.587,477.773,132.798,477.773,132.798 Z" />
<path
android:fillColor="#e85240"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M192.262,268.852 C192.262,268.852,176.787,290.885,189.639,304.262 C232.393,304.524,232.918,304.787,232.918,304.787 C232.918,304.787,244.197,296.394,238.426,272.787 C212.983,262.82,192.262,268.853,192.262,268.853 Z" />
<path
android:fillColor="#e85240"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M325.521,272.819 C342.546,273.885,364.341,295.901,343.357,321.081 C341.783,321.606,294.372,315.775,293.783,311.901 C292.977,306.601,285.826,270.333,325.521,272.819 Z" />
<path
android:fillColor="#fbb579"
android:strokeWidth="7.14054"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M293.046,306.321 C295.599,297.786,313.477,296.791,323.835,298.955 C333.994,301.077,349.503,308.913,346.293,317.177 C344.717,321.235,331.038,319.114,331.038,319.114 C331.038,319.114,289.756,317.315,293.045,306.321 Z" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M325.521,272.819 C342.546,273.885,364.341,295.901,343.357,321.081 C341.783,321.606,294.372,315.775,293.783,311.901 C292.977,306.601,285.826,270.333,325.521,272.819 Z" />
<path
android:strokeColor="#ebc2bf"
android:strokeWidth="2.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M309.328,258.435 C309.328,258.435,335.558,253.189,346.049,262.632" />
<path
android:fillColor="#ffffff"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M329.634,284.993 A7.8688526,7.0819674,0,0,1,321.765,292.075 A7.8688526,7.0819674,0,0,1,313.896,284.993 A7.8688526,7.0819674,0,0,1,321.765,277.911 A7.8688526,7.0819674,0,0,1,329.634,284.993 Z" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="14.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M170.492,266.492 C170.492,266.492,202.492,246.558,240.787,264.394" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="14.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M292.997,274.177 C292.997,274.177,328.548,261.627,362.085,287.315" />
<path
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M431.034,176.197 L449.21,158.763 L490.755,212.92" />
<path
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M454.032,267.449 L497.247,243.523" />
<path
android:fillColor="#fff9f6"
android:fillAlpha="0"
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M164.239,424.641 L133.168,450.694 C133.168,450.694,127.975,468.87,159.134,479.627 C190.293,490.384,372.425,481.111,372.425,481.111 C372.425,481.111,424.728,492.981,453.29,444.017 C452.919,444.759,444.387,463.306,444.387,463.306 L445.871,482.595 C445.871,482.595,503.606,473.181,476.327,333.64 C480.524,324.197,484.72,318.427,484.72,318.427 L481.572,296.657 C481.572,296.657,487.23,256.322,489.455,251.128 C491.681,245.935,497.971,244.777,499.099,240 C501.168,231.244,496.317,219.598,492.422,213.849 C491.68,210.511,499.284,178.795,494.462,155.797 C487.414,143.927,489.64,143.927,475.544,130.202 C475.544,130.573,484.447,85.3181,474.802,56.7556 C471.834,56.7556,442.53,45.9983,377.986,80.4958 C377.615,81.6086,351.649,50.4496,224.416,64.5453 C223.674,64.5453,181.016,26.7093,157.647,24.1127 C156.534,23.7418,135.02,20.0323,132.423,73.0769 C132.423,74.1897,88.281,89.0274,76.4109,137.992 C64.5408,186.956,65.2827,218.857,65.2827,218.857 C65.2827,218.857,46.3778,246.52,55.4846,243.89 C60.8314,242.346,60.4605,267.45,60.4605,267.45 L58.2349,306.77 C58.2349,306.77,43.7682,342.009,43.3972,366.492 C43.0263,390.974,42.6553,400.619,48.2194,415.085 C53.7835,429.552,68.9921,461.824,90.8776,478.145 C99.7802,478.516,109.796,480,109.796,480 L113.505,459.227 C113.505,459.227,132.423,475.177,145.777,474.065 C159.131,472.952,145.035,473.323,145.035,473.323 L131.31,455.518 Z" />
<path
android:strokeColor="#c3b4b0"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M197.508,220.066 L215.869,207.738" />
<path
android:strokeColor="#c3b4b0"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M327.541,234.249 C327.541,234.249,321.269,239.154,311.683,233.258" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M322.623,86.0328 C322.623,86.0328,398.056,112.153,432.787,175.213 C466.23,235.934,465.836,237.639,465.836,237.639 C465.836,237.639,466.361,246.032,445.902,249.18 C443.279,249.18,460.591,280.131,465.836,286.426 C451.147,293.77,449.049,295.344,449.049,295.344 C449.049,295.344,455.344,303.213,453.77,311.082 C436.459,310.557,431.213,310.033,431.213,310.033 C431.213,310.033,427.016,327.344,415.475,343.082 C410.23,333.115,410.23,320,410.23,320 C410.23,320,386.098,356.721,337.836,361.443 C336.262,358.82,350.426,347.279,355.672,333.115 C344.131,334.689,340.983,335.213,340.983,335.213 L351.475,318.426 C351.475,318.426,362.491,294.295,360.918,290.098 C359.869,287.475,328.918,232.393,328.918,232.393 C328.918,232.393,304.262,265.442,285.377,282.229 C278.557,278.032,271.738,267.54,271.738,267.54 L259.148,279.081 C259.148,279.081,210.71,228.317,217.054,188.898 C213.906,200.964,217.181,245.507,211.411,251.278 C206.165,249.18,195.149,236.589,197.247,210.36 C196.722,209.835,167.345,251.803,167.345,251.803 C167.345,251.803,163.148,300.59,183.607,325.246 C185.181,327.869,182.033,341.508,152.132,322.623 C153.181,321.049,151.083,332.59,151.083,332.59 C151.083,332.59,104.394,334.688,105.444,271.738 C102.821,269.115,91.8047,305.836,91.8047,305.836 C91.8047,305.836,67.6735,273.836,91.2801,212.984 C103.083,182.558,114.624,160.132,123.215,145.312 C131.805,130.492,137.445,123.279,137.445,123.279" />
<path
android:fillColor="#a88a8f"
android:strokeColor="#4c4f59"
android:strokeWidth="2.8623"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M216.292,255.539 A8.8904896,7.4928694,0,0,1,207.402,263.032 A8.8904896,7.4928694,0,0,1,198.512,255.539 A8.8904896,7.4928694,0,0,1,207.402,248.046 A8.8904896,7.4928694,0,0,1,216.292,255.539 Z" />
<path
android:fillColor="#a88a8f"
android:strokeColor="#4c4f59"
android:strokeWidth="2.8623"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M336.169,270.547 A8.8904896,7.4928694,0,0,1,327.279,278.04 A8.8904896,7.4928694,0,0,1,318.389,270.547 A8.8904896,7.4928694,0,0,1,327.279,263.054 A8.8904896,7.4928694,0,0,1,336.169,270.547 Z" />
<path
android:fillAlpha="0"
android:strokeColor="#a18f90"
android:strokeWidth="10.4"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M318.719,85.4618 C318.719,85.4618,369.167,102.525,395.504,128.862 C421.841,155.199,428.147,168.182,428.147,168.182" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="6.5"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M186.754,224.525 L197.246,225.574" />
<path
android:fillColor="#fcbeb5"
android:strokeColor="#e99c9b"
android:strokeWidth="3.4"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M250.942,349.983 C250.942,349.983,237.032,341.08,240.927,333.291 C244.822,325.501,258.732,328.84,258.732,328.84 C258.732,328.84,271.715,327.542,278.763,324.945 C285.811,322.348,288.407,329.953,288.407,329.953 C288.407,329.953,293.172,336.018,284.141,346.46 C278.206,353.322,262.997,353.508,262.997,353.508" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e99c9b"
android:strokeWidth="1.8"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M276.209,325.765 C276.209,325.765,280.559,328.784,281.584,333.717 C282.155,333.823,284.918,330.63,284.56,326.844 C283.88,325.359,276.209,325.764,276.209,325.764 Z" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e99c9b"
android:strokeWidth="1.8"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M245.007,329.396 L240.834,332.178 L240.741,338.206 L240.645,338.171 L247.718,329.354 Z" />
<path
android:strokeColor="#e99c9b"
android:strokeWidth="3.4"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M250.942,349.983 C250.942,349.983,237.032,341.08,240.927,333.291 C244.822,325.501,258.732,328.84,258.732,328.84 C258.732,328.84,271.715,327.542,278.763,324.945 C285.811,322.348,288.407,329.953,288.407,329.953 C288.407,329.953,293.172,336.018,284.141,346.46 C278.206,353.322,262.997,353.508,262.997,353.508" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e89493"
android:strokeWidth="2.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M190.849,314.929 L196.97,329.952" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e89493"
android:strokeWidth="2.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M204.945,316.969 L210.509,331.25" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e89493"
android:strokeWidth="2.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M217.928,317.34 L225.347,333.847" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e89493"
android:strokeWidth="2.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M300.648,328.468 L307.51,346.644" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e89493"
android:strokeWidth="2.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M312.703,329.21 L319.38,346.273" />
<path
android:fillColor="#ffffff"
android:strokeColor="#e89493"
android:strokeWidth="2.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M326.243,328.654 L333.291,346.645" />
<path
android:strokeColor="#e49589"
android:strokeWidth="4.6"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M218.492,348.984 C216.619,350.289,215.156,352.098,213.789,353.9 C212.764,355.251,211.787,356.65,211.017,358.164" />
<path
android:strokeColor="#4c4f59"
android:strokeWidth="3.2"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M127.475,375.082 C127.475,375.082,113.049,355.934,115.409,315.541" />
<path
android:fillColor="#ffffff"
android:fillAlpha="0.734285"
android:strokeColor="#c1545a"
android:strokeWidth="7.8"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M136.393,322.098 C136.393,322.098,145.836,303.213,132.196,282.229 C129.048,281.704,159.999,254.426,137.442,205.114 C135.868,205.639,161.049,149.507,113.311,122.229 C109.114,113.836,67.6717,52.9831,24.1307,135.344 C24.1307,146.885,23.0815,171.541,23.0815,171.541 C23.0815,171.541,8.393,210.361,16.7864,243.41 C18.8848,246.558,-4.1972,293.246,27.8028,349.902 C28.3274,350.427,11.0159,407.607,40.393,432.787 C41.4422,437.508,54.0324,466.361,82.8848,472.131 C95.475,472.656,102.295,471.606,102.295,471.606 C102.295,471.606,133.77,466.885,150.033,404.458 C147.935,376.655,145.836,354.097,139.017,333.638" />
<path
android:fillColor="#ffffff"
android:fillAlpha="0.734285"
android:strokeColor="#000000"
android:strokeWidth="9"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M109.698,219.489 C109.698,219.489,94.4894,207.619,88.5543,199.829 M51.4602,219.86 L73.7167,197.974 M74.0876,183.878 L80.0227,225.794 M44.7833,201.683 L100.795,196.861 M111.923,179.427 L92.634,179.798 M92.2631,179.056 L90.7793,149.381 M51.8305,152.719 L89.6665,149.751 M64.8134,138.252 L65.5553,177.943" />
<path
android:fillColor="#ffffff"
android:fillAlpha="0.734285"
android:strokeColor="#000000"
android:strokeWidth="9"
android:strokeLineJoin="round"
android:strokeMiterLimit="1.3"
android:strokeLineCap="round"
android:pathData="M58.5078,352.286 L112.665,348.948 M66.2973,335.223 L101.908,332.255 M80.3934,299.983 L82.99,333.368 M64.072,317.047 L101.166,313.709 M102.65,332.256 L103.392,298.5 M103.021,298.871 L62.2175,301.097 M62.2175,300.726 L66.6688,334.111 M94.1184,285.889 L97.4569,271.422 M97.4569,269.938 L70.0072,271.422 M68.5234,286.26 L69.2653,266.229" />
<path
android:fillColor="#d96477"
android:strokeWidth="0.00475452"
android:pathData="M69.454,376.47 C65.2333,376.691,61.3746,378.322,58.6708,381.019 C55.6903,384.001,54.4288,388.12,55.1367,392.567 C56.0415,398.241,59.9003,404.306,66.5213,410.469 C67.9264,411.773,68.9164,412.618,70.9123,414.211 C74.4198,417.006,77.5919,419.223,81.9616,421.92 C83.5211,422.888,86.5974,424.685,87.7577,425.309 L88.093,425.492 L88.5827,425.224 C90.3657,424.243,93.5432,422.361,95.4273,421.172 C102.554,416.674,108.26,412.159,112.491,407.666 C119.352,400.378,122.268,393.281,120.922,387.173 C119.991,382.968,116.877,379.49,112.459,377.723 C110.559,376.963,108.712,376.568,106.62,376.474 C103.911,376.351,101.244,376.814,98.7003,377.846 C94.3679,379.6,90.7486,383.019,88.4706,387.513 C88.3109,387.827,88.1619,388.103,88.1406,388.12 C88.0714,388.175,87.9969,388.069,87.7095,387.491 C87.0282,386.132,85.6923,384.157,84.6438,382.968 C83.5687,381.741,81.8389,380.245,80.5509,379.426 C77.2244,377.307,73.3284,376.27,69.4537,376.47 Z" />
</group>
</vector> </vector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.135"
android:scaleY="0.135">
<path
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
android:strokeWidth="18"
android:strokeColor="#1e110d" />
<path
android:fillColor="#1e110d"
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
<path
android:fillColor="#1e110d"
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
</group>
</vector>

View File

@@ -1,22 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="512dp"
android:height="108dp" android:height="512dp"
android:viewportWidth="108" android:viewportWidth="512"
android:viewportHeight="108"> android:viewportHeight="512">
<path
<group android:pathData="m212.06,292.39c0,0 -14.77,-11.53 -20.53,-19.09m-36.03,19.45 l21.62,-21.26m0.36,-13.69 l5.76,40.71m-34.22,-23.42 l54.4,-4.68m10.81,-16.93 l-18.73,0.36m-0.36,-0.72 l-1.44,-28.82m-37.83,3.24 l36.75,-2.88m-24.14,-11.17 l0.72,38.55"
android:scaleX="0.135" android:strokeLineJoin="round"
android:scaleY="0.135"> android:strokeWidth="8.74077"
<path android:fillColor="#ffffff"
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z" android:strokeColor="#000000"
android:strokeWidth="18" android:fillAlpha="0.734285"
android:strokeColor="#000000" /> android:strokeLineCap="round"/>
<path <path
android:fillColor="#000000" android:pathData="m239.47,294.6 l48.58,-2.99m-41.59,-12.31 l31.94,-2.66m-19.3,-28.95 l2.33,29.94m-16.97,-14.64 l33.27,-2.99m1.33,16.64 l0.67,-30.28m-0.33,0.33 l-36.6,2m0,-0.33 l3.99,29.94m24.62,-43.25 l2.99,-12.98m0,-1.33 l-24.62,1.33m-1.33,13.31 l0.67,-17.97"
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" /> android:strokeLineJoin="round"
<path android:strokeWidth="8.07261"
android:fillColor="#000000" android:fillColor="#ffffff"
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" /> android:strokeColor="#000000"
</group> android:fillAlpha="0.734285"
</vector> android:strokeLineCap="round"/>
<path
android:pathData="m319.56,229.67c-3.9,0.24 -7.46,1.99 -9.95,4.89 -2.75,3.2 -3.91,7.63 -3.26,12.41 0.84,6.1 4.4,12.62 10.51,19.24 1.3,1.4 2.21,2.31 4.05,4.02 3.24,3 6.16,5.39 10.2,8.29 1.44,1.04 4.28,2.97 5.35,3.64l0.31,0.2 0.45,-0.29c1.65,-1.05 4.58,-3.08 6.32,-4.36 6.58,-4.83 11.84,-9.69 15.75,-14.52 6.33,-7.83 9.02,-15.46 7.78,-22.03 -0.86,-4.52 -3.73,-8.26 -7.81,-10.16 -1.75,-0.82 -3.46,-1.24 -5.39,-1.34 -2.5,-0.13 -4.96,0.37 -7.31,1.47 -4,1.89 -7.34,5.56 -9.44,10.39 -0.15,0.34 -0.28,0.63 -0.3,0.65 -0.06,0.06 -0.13,-0.05 -0.4,-0.68 -0.63,-1.46 -1.86,-3.58 -2.83,-4.86 -0.99,-1.32 -2.59,-2.93 -3.78,-3.81 -3.07,-2.28 -6.67,-3.39 -10.24,-3.18z"
android:strokeWidth="0.00473542"
android:fillColor="#000000"
android:strokeColor="#00000000"/>
</vector>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.135"
android:scaleY="0.135">
<path
android:pathData="M 259 259 H 541 V 541 H 259 V 259 Z"
android:strokeWidth="18"
android:strokeColor="#000000" />
<path
android:fillColor="#000000"
android:pathData="M 257 257 H 407 V 407 H 257 V 257 Z" />
<path
android:fillColor="#000000"
android:pathData="M 393 393 H 543 V 543 H 393 V 393 Z" />
</group>
</vector>

View File

@@ -0,0 +1,42 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,21l-8,-4.5v-9l8,-4.5l8,4.5v4.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12l8,-4.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12v9"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,12l-8,-4.5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M22,18h-7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
<path
android:pathData="M18,15l-3,3l3,3"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:strokeColor="#ffff"
android:strokeLineCap="round"/>
</vector>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome" /> <monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground_alt"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome_alt" />
</adaptive-icon>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground_alt"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome_alt" />
</adaptive-icon>

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