Compare commits

...

441 Commits
v23.0 ... v24.1

Author SHA1 Message Date
topjohnwu
32fc34f922 Release Magisk v24.1 2022-01-28 03:43:42 -08:00
topjohnwu
b82a393692 Add v24.1 release notes 2022-01-28 03:37:00 -08:00
LoveSy
3c7e792167 Catch PendingIntent.CanceledException thrown from send 2022-01-27 05:29:32 -08:00
LoveSy
0ad66875ab Fix crash when zip is malformat
Co-authored-by: 南宫雪珊 <vvb2060@gmail.com>
Co-authored-by: 残页 <31466456+canyie@users.noreply.github.com>
2022-01-27 05:26:31 -08:00
Arbri çoçka
1191ac2671 update Albania translation 2022-01-27 05:25:13 -08:00
topjohnwu
928b3425e3 Embed module installer in APK 2022-01-27 05:24:05 -08:00
topjohnwu
0726a00e3b Fix download notifications 2022-01-27 05:17:52 -08:00
LoveSy
5a88984d34 Guard synchronizedList's iteration
It's needed to guard a synchronizedList when iterating it
2022-01-27 02:01:30 -08:00
LoveSy
18de60f68c Fix NPE of SuRequestViewModel
countdown timer may have not initialized when backpressed
2022-01-27 02:01:04 -08:00
LoveSy
1893359142 Fix crash when fragment is detached from activity 2022-01-27 01:54:24 -08:00
topjohnwu
f5e5ab2436 Update Android Studio 2022-01-27 01:46:00 -08:00
topjohnwu
ff5ea1a70d Clarify what 64-bit only means 2022-01-26 04:39:14 -08:00
topjohnwu
54ee63a409 Minor install guide changes 2022-01-26 02:55:25 -08:00
topjohnwu
f095606b50 Release new canary build 2022-01-26 02:41:46 -08:00
topjohnwu
e8f31c78d7 Update README 2022-01-26 02:33:22 -08:00
topjohnwu
b34c477d5e Release Magisk v24.0 2022-01-26 02:21:22 -08:00
topjohnwu
28611304f7 Add v24.0 release notes 2022-01-26 02:08:49 -08:00
CISZEK Anthony
76af9e6e1f Update French translations 2022-01-26 00:56:39 -08:00
topjohnwu
7b3b965ed7 Fix some typos 2022-01-26 00:52:51 -08:00
topjohnwu
567b905ef1 Update install guides 2022-01-26 00:48:16 -08:00
topjohnwu
a94268329c Update developer guide 2022-01-25 23:05:03 -08:00
Oliver Cervera
a11a18686a Update italian translation
Contains last changes, it is also synced with the main file.
2022-01-25 21:56:04 -08:00
AndroPlus
c58e3a99ee Update Japanese translation 2022-01-25 21:55:50 -08:00
topjohnwu
b166663e89 Release new canary build 2022-01-25 05:03:34 -08:00
topjohnwu
ac13ac14f6 Remove deploy.md 2022-01-25 04:26:59 -08:00
topjohnwu
06531f6d06 Add annotations to suppress warnings 2022-01-25 04:16:14 -08:00
topjohnwu
f6274d94f6 Add setprop warnings 2022-01-25 03:52:46 -08:00
topjohnwu
2b303a7e23 Add a missing busybox patch back
Hopefully, fix #4174
2022-01-25 03:37:59 -08:00
topjohnwu
2bb074a5ad Update developer guides 2022-01-25 03:04:23 -08:00
topjohnwu
3b2db56243 Update documentation 2022-01-25 02:32:52 -08:00
topjohnwu
45483fde74 Update CLI usages 2022-01-25 02:04:15 -08:00
topjohnwu
d742cfa48f Label Zygisk as beta 2022-01-25 01:41:51 -08:00
topjohnwu
95353ce9eb Fix language settings 2022-01-25 01:31:15 -08:00
topjohnwu
ab2cc72814 Remove unnecessary root service connection wait 2022-01-25 01:21:21 -08:00
topjohnwu
5c54a2c008 Update version check logic 2022-01-25 01:10:17 -08:00
topjohnwu
2fe3082518 Update busybox 2022-01-24 23:20:36 -08:00
topjohnwu
5a889d28c8 Pick a more reasonable zopfli config
Close #4980
2022-01-24 23:07:49 -08:00
Vlad
45e7c1c030 Update RU strings 2022-01-24 22:30:53 -08:00
topjohnwu
c6dcff0ae7 Minor dynamic_bitset changes 2022-01-24 22:30:19 -08:00
Hen Ry
b791dc5e1a Update de translation 2022-01-24 21:32:07 -08:00
DanGLES3
46db281006 Update pt-BR strings 2022-01-24 21:31:48 -08:00
vvb2060
636479b15b Fix dynLoad 2022-01-24 21:31:27 -08:00
vvb2060
dcbb4eabb5 Fix string 2022-01-24 21:27:17 -08:00
vvb2060
068cedaa84 Update zh-rCN translation 2022-01-24 03:05:45 -08:00
LoveSy
02dd962601 Don't load zygisk module for magisk app 2022-01-24 03:05:12 -08:00
topjohnwu
256d715648 Release new canary build 2022-01-23 07:07:21 -08:00
topjohnwu
cbe97cdfde Fix dynamic_bitset implementation 2022-01-23 04:39:00 -08:00
topjohnwu
407dfc7547 Always write 0 to fd 2022-01-23 04:19:07 -08:00
Arbri çoçka
a8e4e077ec Update Albania translator 2022-01-23 02:45:18 -08:00
vvb2060
3d06ba1878 Use WindowCompat 2022-01-23 02:44:59 -08:00
topjohnwu
8a23d1da58 Do not run setMarkdown on I/O thread 2022-01-23 02:41:56 -08:00
topjohnwu
d3eb61e0e4 Fix string resources 2022-01-23 02:41:56 -08:00
vvb2060
7cdf2d244d Cleanup su handler 2022-01-23 01:38:17 -08:00
topjohnwu
c59a41a607 Minor code refactoring 2022-01-23 01:08:09 -08:00
topjohnwu
e0410b6f10 TLS only on release builds 2022-01-22 22:57:34 -08:00
topjohnwu
8eac6c0b48 Cleanup arch classes 2022-01-22 14:44:46 -08:00
vvb2060
bf8b74e996 Module json add changelog 2022-01-22 14:44:37 -08:00
kubalav
691e41e22e Update Slovak translation 2022-01-22 05:31:28 -08:00
AioiLight
15e91d42ee Update strings.xml 2022-01-22 05:31:14 -08:00
vvb2060
5e8e94fd0f Remove emoji compat 2022-01-22 05:30:59 -08:00
topjohnwu
5313a46aa2 Overhaul SettingsItem
Close #5021
2022-01-22 05:25:36 -08:00
topjohnwu
761a8dde65 Slightly change update UI logic 2022-01-21 05:37:47 -08:00
topjohnwu
a73acfb9c2 Show unloaded Zygisk modules in UI 2022-01-21 05:37:47 -08:00
topjohnwu
fbe17dde03 Add flag for unloaded Zygisk modules 2022-01-21 05:37:47 -08:00
vvb2060
a01a3404fe Ignore duplicate clicks on BottomNavigationView
Co-authored-by: RikkaW <rikka@shizuku.moe>
2022-01-21 02:12:41 -08:00
canyie
454e5dfc5d Show confirmation dialog before restore app 2022-01-21 01:35:11 -08:00
topjohnwu
47545b45b8 Clean up MagiskDialog 2022-01-21 00:50:02 -08:00
topjohnwu
7c9908d953 Release new canary build 2022-01-20 03:58:16 -08:00
canyie
5f4cd50cc4 Properly prevent fix env dialog from constantly showing
ViewModel has been reconstructed when switching fragment so we lost previous state.
2022-01-20 03:38:28 -08:00
canyie
b0fba6ce5b Optimize navigation back stack
Fix topjohnwu#4333

Co-authored-by: LoveSy <shana@zju.edu.cn>
2022-01-20 03:38:00 -08:00
canyie
1f5992f2c2 Fix classloader when restoring bottom nav state 2022-01-20 03:36:31 -08:00
topjohnwu
abfd3c3e5d Remove unused resources 2022-01-20 03:32:08 -08:00
LoveSy
97da7f9691 Do not trust format of 3rd party json 2022-01-20 02:02:39 -08:00
Ilya Kushnir
2752083d29 Update RU strings 2022-01-20 01:59:33 -08:00
John Wu
c826318da4 Update CLI usage text 2022-01-20 01:59:01 -08:00
LoveSy
6582a4abd9 Make magiskpolicy supports multiple --apply 2022-01-20 01:59:01 -08:00
topjohnwu
a699dab5b3 Add option to skip building for AVD commands 2022-01-20 01:42:41 -08:00
topjohnwu
21c8ad5b9e Fix up some AVD scripts 2022-01-20 01:33:59 -08:00
topjohnwu
195d885887 Reduce log spamming 2022-01-20 00:18:46 -08:00
topjohnwu
519bd2f30f Disable AVD hacks by default 2022-01-19 20:28:01 -08:00
topjohnwu
20ef724fad Add new build command avd_patch 2022-01-19 05:12:11 -08:00
vvb2060
f443cbaa2b Revert "Always run non disabled module post-fs-data scripts"
This reverts commit 4dfb193d10.
2022-01-18 04:48:47 -08:00
vvb2060
dbf45da8ab Avoid constantly check env 2022-01-18 04:47:53 -08:00
topjohnwu
6b67902d53 Uninstalled app su requests should still show in logs 2022-01-18 04:44:11 -08:00
topjohnwu
0ad0ef485c Bump min Magisk version to v21.0
Close #5220
2022-01-18 04:27:40 -08:00
topjohnwu
7dfe3e53d5 Optimize imports 2022-01-18 03:58:47 -08:00
vvb2060
5be3bd1e64 Show User-Agent 2022-01-18 02:02:41 -08:00
vvb2060
bc0c1980db Support modules update 2022-01-18 02:02:41 -08:00
vvb2060
2997258fd0 Cleanup code 2022-01-18 02:02:41 -08:00
vvb2060
11600fc116 Use libs instead of copy code 2022-01-18 02:02:41 -08:00
vvb2060
a8640f52ef Merge into one file 2022-01-18 02:02:41 -08:00
Arbri çoçka
0f4e44c38f Update values-sq 2022-01-18 00:35:55 -08:00
capntrips
053f4d481d disable virtualAB check for noSecondSlot 2022-01-18 00:35:38 -08:00
capntrips
f466c27da9 disable pixel check for noSecondSlot 2022-01-18 00:35:38 -08:00
RikkaW
bfe6bc3095 Fix bottom nav sometimes not hide correctly
Replace homemade animation with StateListAnimator
2022-01-18 00:24:16 -08:00
vvb2060
ff8f3e766e Update zh-rCN translation 2022-01-18 00:23:40 -08:00
vvb2060
6635ea3e29 Allow offline hide manager 2022-01-18 00:20:49 -08:00
LoveSy
591788c0df Fix an NPE
java.lang.NullPointerException: Attempt to invoke virtual method 'int android.text.Layout.getEllipsisCount(int)' on a null object reference
2022-01-17 22:55:44 -08:00
vvb2060
571b8986a4 Don't assume installed version matches app 2022-01-17 20:29:11 -08:00
topjohnwu
bb7a74e4b4 Add Zygisk API getFlags() 2022-01-17 19:54:33 -08:00
topjohnwu
76ddfeb93a Allow modifying denylist without enforcement 2022-01-15 23:46:22 -08:00
LoveSy
c38b826abf Skip overlayfs for post-fs-data mount
adb remount will introduce overlayfs for /system and /vendor, we should
skip mounting as overlayfs. This also helps us support overlayfs Magisk
later.
2022-01-14 03:42:37 -08:00
topjohnwu
21d7db0959 Add new Zygisk API to get module dir 2022-01-14 03:10:02 -08:00
topjohnwu
d7b51d2807 Update dependencies 2022-01-14 00:07:19 -08:00
topjohnwu
a7af8b5722 Add DoH back
JSDelivr is no longer China friendly
2022-01-13 03:50:29 -08:00
topjohnwu
9c93fe6003 Update bootctl
Close #5134
2022-01-13 02:24:02 -08:00
topjohnwu
21505a7470 Update scripts for PATCHVBMETAFLAG 2022-01-12 02:29:34 -08:00
topjohnwu
ba6e6cc15a Update vbmeta option hiding criteria
Hide when Samsung, A/B, or vbmeta partition exists
2022-01-12 02:29:34 -08:00
vvb2060
fd7bf2bc3a Support PATCHVBMETAFLAG env variable 2022-01-12 02:29:34 -08:00
LoveSy
b2cd24ed1b Fix an UB when cil compile error 2022-01-11 03:01:27 -08:00
vvb2060
66cf2c984a Don't fix env when patch boot 2022-01-11 02:50:12 -08:00
残页
de1b2b19b0 Only store sepolicy rules into partitions in ext4 format
Fix topjohnwu#5013
When installing from recovery, previous implementation may select f2fs partitions to store sepolicy rules, but magiskinit won't mount them and unable to load sepolicy rules.
2022-01-11 02:25:34 -08:00
LoveSy
e31583485d Don't prefetch env to avoid deadlock
Fix #5178
2022-01-11 00:47:06 -08:00
topjohnwu
490e51c1d7 Don't set RECOVERYMODE if recovery_dtbo exists
Apparently some boot images with ramdisk still have recovery_dtbo,
so this assumption is no longer safe to do. Expect the user to
set this option properly themselves in the app.

Fix #4976, close #5070, close #5184
2022-01-11 00:17:47 -08:00
RikkaW
1df2a04713 Find suitable anchor view for SnackBar
This will fix the SnackBar position if BottomNav or FAB is visible.

Fix #5163
Fix #5135
2022-01-10 23:10:42 -08:00
vvb2060
42804d5314 Fix stub clean task 2022-01-10 23:02:51 -08:00
vvb2060
558710bbdd Fix gradle task dependencies 2022-01-02 16:10:44 -08:00
topjohnwu
f4926cb822 Small refactoring 2022-01-02 16:09:03 -08:00
topjohnwu
1e77e0862a Separate fstab finding to its own function 2022-01-02 15:49:12 -08:00
topjohnwu
8c696cb8ca Minor code refactoring 2021-12-28 23:37:06 -08:00
LoveSy
62ef8ade8f Skip loading Magisk when detecting DSU
Fix #4402

Co-authored-by: topjohnwu <topjohnwu@gmail.com>
2021-12-28 21:04:09 -08:00
LoveSy
3d88dd3123 Update dtc to fix a UB
See https://github.com/dgibson/dtc/pull/65
2021-12-28 17:18:32 -08:00
残页
880b348ce6 Add an old cgroup path
Fix topjohnwu#5125
cgroup root path might be mem cgroup instead of acct, especially on low-ram devices.
bc131c3244%5E%21/#F0
2021-12-28 17:12:15 -08:00
残页
31fe3a1cd8 Java keywords cannot be used as package/class name 2021-12-28 17:11:38 -08:00
LoveSy
19182ffddf If dt fstab contains error, fallback to default fstab
See https://cs.android.com/android/platform/superproject/+/master:system/core/init/first_stage_mount.cpp;drc=master;l=155

From the source of `FirstStageMount`, dt fstab can fail gracefully and
if any error occurs it will fall back to default fstab. Magisk now
replaces the default fstab and dt fstab unconditionally, bringing potential
errors to the default fstab and causing init fails to load partitions.
2021-12-28 17:10:52 -08:00
vvb2060
afcc60066e Fix toast 2021-12-27 12:17:35 -08:00
vvb2060
d3ade06421 Use InputStream transfer 2021-12-27 12:17:35 -08:00
topjohnwu
f1a3ef9590 Update dependencies 2021-12-27 12:17:09 -08:00
Arbri çoçka
d1d73f11a5 fix same text in Albania 2021-12-27 12:01:05 -08:00
topjohnwu
05697372f8 Remove issues action
Handled by external MagiskBot
2021-12-25 17:26:03 -08:00
topjohnwu
0c1f68816e Release new canary build 2021-12-14 21:40:40 -08:00
kubalav
92546e8a74 Update Slovak translation 2021-12-14 21:25:58 -08:00
John Wu
a4faa3f392 Update stub strings.xml path 2021-12-14 05:31:12 -08:00
南宫雪珊
df191cd2b5 Use AGP to compile resources 2021-12-14 05:30:15 -08:00
南宫雪珊
baa19f0ccf Rewrite app installation
Fix  #4960
2021-12-14 05:20:29 -08:00
vvb2060
5a49bd3ac9 Add OkHttp cache 2021-12-14 05:01:12 -08:00
LoveSy
b37d7e0500 Use default icon when failed to get app icon
Fix #5051
2021-12-14 04:58:18 -08:00
topjohnwu
f4ed6274a4 Invert vbmeta header patching config
vbmeta header should not be patched in most cases
2021-12-14 04:52:25 -08:00
LoveSy
56eb1a1cf9 Load fstab from system/etc
Caused by this commit: e98afa2687

Fix #5057
2021-12-14 03:51:55 -08:00
LoveSy
a7c156a9e3 Further fix oplus.fstab support
* Further fix `oplus.fstab` support

In some oneplus devices, `oplus.fstab` does exists but `init` never
loaded it and those entries in `oplus.fstab` are written directly to
`fstab.qcom`. Previous implementation will introduce duplicate entries
to `fstab.qcom` and brick the device. This commit filters those entries
from `oplus.fstab` that are already in `fstab.qcom` and further filters
duplicated entries in `oplus.fstab` (keep only the last entry).

Fix #5016

* Fix UB

Since we moved entry, we need to explicitly copy its member.
For c++23 we can use `auto{}`.
2021-12-14 03:40:23 -08:00
南宫雪珊
d81ca77231 Update gradle/wrapper/gradle-wrapper.properties 2021-12-14 03:36:01 -08:00
南宫雪珊
bf013f6ebb Fix Build 2021-12-14 03:36:01 -08:00
vvb2060
dd8116e285 Update Dependencies 2021-12-14 03:36:01 -08:00
残页
b5d80a88d1 Only care about mount namespace isolating 2021-12-14 03:08:55 -08:00
vvb2060
7f4f95cf83 Fix certificate start time 2021-12-14 03:07:38 -08:00
LoveSy
87c2f6ad14 xhook_clear after xhook_refresh 2021-12-14 03:06:57 -08:00
topjohnwu
ad47dba064 Rename magiskVersion* -> version* 2021-12-14 03:01:11 -08:00
LoveSy
41b701846f ensureEnv even if getRemote fails
Co-authored-by: vvb2060 <vvb2060@gmail.com>
2021-12-14 02:57:58 -08:00
xz-dev
5c42830328 l10n: Fix typo of chinese Simplified translations 2021-12-14 02:52:38 -08:00
Allan Nordhøy
69617309f8 English language string fixes 2021-12-14 02:51:54 -08:00
topjohnwu
48e2d6a8da Simplify several hacks 2021-12-13 19:48:17 -08:00
topjohnwu
b4120cddfb IODispatcherExecutor -> DispatcherExecutor 2021-12-13 04:05:42 -08:00
topjohnwu
54e3f1998a Support RootService on stub APKs 2021-12-13 04:05:42 -08:00
topjohnwu
edcf9f1b0c Introduce RootServices to the app 2021-12-13 04:05:42 -08:00
topjohnwu
de3747d65e Copy APK from external storage in stub
Much faster and easier development
2021-12-13 04:05:42 -08:00
vvb2060
b76a3614da Fix isolated process comparisons 2021-12-10 04:35:38 -08:00
topjohnwu
94cc64c51b Update dependencies 2021-12-10 04:32:16 -08:00
HeroBuxx
0f71edee96 magisk: README: Correct string path for stub
Signed-off-by: HeroBuxx <herobuxx@conqueros.co>
2021-12-01 21:20:24 -08:00
topjohnwu
e097c097fe Rename persist_properties.cpp -> persist.cpp 2021-11-30 01:58:31 -08:00
topjohnwu
1443a5b175 Use mmap_data more widely 2021-11-30 01:50:55 -08:00
topjohnwu
2d82ad93dd Macro -> template 2021-11-29 19:56:37 -08:00
vvb2060
384c257a74 Disable CompatVectorFromResources 2021-11-29 00:06:28 -08:00
vvb2060
49dfa2c3a0 Fix update from notification will fail 2021-11-29 00:05:54 -08:00
vvb2060
7bd3e768db Remove bytecode compatibility workaround 2021-11-29 00:05:20 -08:00
vvb2060
65224ed22b Fix NPE when apk could not be parsed 2021-11-29 00:04:51 -08:00
topjohnwu
0a28dfe1e2 AVB blobs expect to be 4096-byte aligned 2021-11-28 13:21:05 -08:00
topjohnwu
1c8ebfacb0 Release new canary build 2021-11-23 22:39:15 -08:00
HuskyDG
5d6d241791 Update VN strings.xml 2021-11-23 22:24:51 -08:00
jontaix
4f116d15b9 Fix PT-rBR translation
Some translation fixes.
2021-11-23 22:24:22 -08:00
topjohnwu
228570640e Introduce KEEPVBMETAFLAG env variable
Close #4447, close #4906, close #4901, close #4964
2021-11-23 22:14:12 -08:00
topjohnwu
65a79610aa Fix crash and warnings 2021-11-23 18:46:06 -08:00
topjohnwu
24984ea4f2 Optimize stream for full-file writes 2021-11-23 18:08:14 -08:00
topjohnwu
048b2af0fc Improve zopfli encoder
Write in chunks for CLI compression
2021-11-23 16:50:08 -08:00
topjohnwu
449989ddd9 Always use zopfli for zImage compression 2021-11-23 14:24:05 -08:00
topjohnwu
01ebe5724a Cleanup zImage parsing code 2021-11-23 13:39:15 -08:00
topjohnwu
95fb230b8c Update to BusyBox 1.34.1 2021-11-22 19:46:52 -08:00
topjohnwu
632971af15 Properly support v4 image headers 2021-11-21 06:07:21 -08:00
topjohnwu
5787aa1078 Stream should always write all bytes 2021-11-21 06:05:59 -08:00
topjohnwu
d8b9265484 Pull out buffer-chunk logic into separate class 2021-11-21 06:05:55 -08:00
topjohnwu
9ea3169ca9 Do not allow modifying page sizes 2021-11-20 22:51:22 -08:00
topjohnwu
aebf2672cd Fix unpacking vendor boot images 2021-11-20 22:44:38 -08:00
osm0sis
68ac409bfd Scripts fixes and improvements
- ensure all scripts use $NVBASE $MAGISKBIN $POSTFSDATAD and $SERVICED where appropriate
- simplify new grep_cmdline() using xargs and more sed
- show correct active sepolicy $RULESDIR on devices with no encryption
- add support for Android 12 .capex (compressed apex) files
2021-11-20 14:17:02 -08:00
topjohnwu
fef44bd24f Allow boot scripts to know Zygisk status 2021-11-20 13:05:15 -08:00
HuskyDG
e4a7617dde Update VN strings.xml 2021-11-16 21:31:21 -08:00
topjohnwu
4dfb193d10 Always run non disabled module post-fs-data scripts 2021-11-16 21:29:13 -08:00
dark-basic
c248d94995 Update strings.xml 2021-11-16 21:15:02 -08:00
vvb2060
d4ac458d17 Ignore zygisk modules when zygisk is not enabled 2021-11-16 21:14:35 -08:00
Ilya Kushnir
93e443c4ad Update RU strings 2021-11-16 21:14:11 -08:00
DanGLES3
4b3988cef9 Update pt-BR translation 2021-11-16 21:13:50 -08:00
Rom
4eb5ee17b4 Fix typo in French translation 2021-11-16 21:13:13 -08:00
topjohnwu
e1b63d7dec Initialize mt19937 statically in function
This reduces startup time
2021-11-16 03:20:07 -08:00
topjohnwu
4b5651bd6f Revert logging after pre specialize 2021-11-16 03:12:01 -08:00
topjohnwu
50515d9128 Close unclosed fds from modules 2021-11-16 01:59:45 -08:00
RikkaW
28b5faab0c Visual changes 2021-11-14 03:58:35 -08:00
topjohnwu
82a01c22d3 Cleanup resources 2021-11-14 00:45:39 -08:00
LoveSy
be9b0c2e8f Move flow.concurrentMap to ktx 2021-11-13 11:28:11 -08:00
LoveSy
b6affe06a5 Fix flow parallel 2021-11-13 11:28:11 -08:00
topjohnwu
1e05f8c646 Release new canary build 2021-11-12 03:14:56 -08:00
topjohnwu
7e9d4512b6 Update zh-rTW 2021-11-12 03:07:18 -08:00
RikkaW
5fa127c415 Disable modules section if Magisk is not active
Fix #4925
2021-11-12 03:02:26 -08:00
kubalav
ac26681fe7 Update Slovak translation 2021-11-12 03:01:49 -08:00
残页
3c62636133 Update zh-rCN translation 2021-11-12 03:01:30 -08:00
Arbri çoçka
ca874fa12c Update Albania translation 2021-11-12 03:00:55 -08:00
Rom
c3508bbb99 Update French translation 2021-11-12 03:00:37 -08:00
topjohnwu
6935033db5 Prevent dangling pointers 2021-11-12 02:02:05 -08:00
topjohnwu
421277d730 Prevent race conditions in connect_companion 2021-11-12 01:55:55 -08:00
topjohnwu
56988944b5 No need to dup fd 2021-11-12 01:54:48 -08:00
topjohnwu
528601d25a Fix integer overflow and workaround seccomp
- Use ftruncate64 instead of ftruncate to workaround seccomp
- Cast uint32_t to off64_t before making it negative

Note: Using ftruncate with a modern NDK libc should actually be
fine as the syscall wrapper in bionic will use ftruncate64 internally.
However, since we are using the libc.a from r10e built for Gingerbread,
seccomp wasn't a thing back then, and also the ftruncate64 symbol is
missing; we have to create our own wrapper and call it instead on
32-bit ABIs.

Props to @jnotuo for discovering the overflow bug and seccomp issue

Fix #3703, close #4915
2021-11-10 03:07:20 -08:00
topjohnwu
ddd153c00d Show module suspend notice
Close #4862
2021-11-09 23:59:37 -08:00
topjohnwu
b8c1588284 Always unload zygisk after specialize 2021-11-07 13:05:44 -08:00
LoveSy
4dac9e40bd Support bootconfig on util_functions.sh
Close #4869
2021-11-07 11:22:21 -08:00
Arbri çoçka
def1811d48 Fix strings in sq 2021-11-07 11:03:28 -08:00
孟武.尼德霍格.龍
c53e507713 Update Traditional Chinese Language 2021-11-07 11:03:15 -08:00
LoveSy
e0ea777249 Use ProcessCompat
Fix #4895
2021-11-07 11:02:58 -08:00
topjohnwu
4c1962f3c7 Release new canary build 2021-11-06 23:56:50 -07:00
Chris Renshaw
258e89c964 Fix script typo for Sony init.real support
- though may still be broken on magiskinit side, see #4885
2021-11-06 23:44:43 -07:00
topjohnwu
3d3bfb42e5 Don't copy ApplicationInfo 2021-11-06 23:34:46 -07:00
topjohnwu
6dbd8baa7e Cleanup DownloadService 2021-11-06 17:45:41 -07:00
topjohnwu
e660fabc57 Remove BaseDownloader 2021-11-06 04:37:06 -07:00
topjohnwu
2115bcd8b0 Relaunch and recreate is slightly different 2021-11-05 16:05:12 -07:00
topjohnwu
1bdd6e1a9d Migrate to Activity Result APIs 2021-11-05 15:53:34 -07:00
topjohnwu
98deec232b Minor adjustments 2021-11-05 13:55:18 -07:00
topjohnwu
022c217cfe Migrate to SplashScreen API 2021-11-05 04:16:58 -07:00
topjohnwu
81f57949ed Remove WorkManager as a dependency 2021-11-04 23:39:35 -07:00
topjohnwu
fca5eb083f Always show checked app in list 2021-11-04 20:09:19 -07:00
topjohnwu
a3695cc66b Use Kotlin coroutine instead of Java parallelStream 2021-11-04 20:09:19 -07:00
topjohnwu
6723d20616 Cleanup AppProcessInfo 2021-11-04 20:09:19 -07:00
RikkaW
627ec91687 Fix visual issues for ActionBar 2021-11-04 20:09:19 -07:00
vvb2060
9126cf0c73 Rewrite deny list UI 2021-11-04 20:09:19 -07:00
Chaosmaster
16322ab30c Use full gzip-signature to find gzip-data.
Fall back to raw image if gzip is not found.

Fixes #4849
2021-11-03 22:23:21 -07:00
Chaosmaster
5682917356 Speed up zopfli compression
See #4810 for example
2021-11-03 22:22:29 -07:00
LoveSy
c91ccc8b4e Fix UB on dtb
`operator==` of string_view will create a tmp `string_view`.
It's an UB if the `const char *` is a nullptr.
`fdt_get_name` however will return a nullptr.
2021-11-03 22:21:48 -07:00
topjohnwu
63f670fc36 Move first stage unload before fork 2021-11-02 21:53:33 -07:00
LoveSy
e20b07fa24 Fix #4853 2021-11-02 19:31:17 -07:00
topjohnwu
472656517f Release new canary build 2021-11-02 04:18:30 -07:00
topjohnwu
d232cba02d Fix first stage unload 2021-11-02 04:12:56 -07:00
vvb2060
e49d29a914 Fix fragments lifecycleOwner 2021-11-02 03:10:29 -07:00
RikkaW
3aa1a68cdc Fix activity relaunches constantly on WSA
It's magic 💢 since change configuration should not trigger activity relaunch.
2021-11-02 03:09:06 -07:00
Hen Ry
f94452083f German Update 2021-11-02 03:08:32 -07:00
Arbri çoçka
ce1ee5cb9d Fix strings in stub Albania 2021-11-02 03:07:44 -07:00
topjohnwu
48df6b8485 Use memmem instead of strstr
It might not be null terminated
2021-10-31 11:46:56 -07:00
topjohnwu
ae23ae2d37 Remove code in scripts that should be removed 2021-10-31 11:30:48 -07:00
Nullptr
e34e04af04 Make Api functions inline
Make Api functions inline to avoid duplicate symbols when including api.hpp in multiple cpps
2021-10-31 10:55:41 -07:00
osm0sis
ff3f377911 scripts: touch up print_title
- stars aren't pounds, let's just call it a title bar :P
2021-10-31 10:53:55 -07:00
osm0sis
18065826b9 scripts: improve basic module setup
- expand utility of the basic module setup (zip without customize.sh) by setting more default perms, since really it couldn't do any simple binary files additions correctly withonly 0755 0644
- ensure CON stays local
2021-10-31 10:53:55 -07:00
topjohnwu
84e19ceef0 Tidy up bootimg.h
Close #4796
2021-10-31 10:52:12 -07:00
Chris Renshaw
59161efd08 Support Samsung 2SI with skip_initramfs in dtb cmdline
Samsung Galaxy A21S and Galaxy M12, probably others, are hdr_v2 boot.img with 2SI judging by the ramdisk contents, but the dtb contains an extra cmdline with skip_initramfs present, even though this shouldn't exist on 2SI and the kernel apparently doesn't even contain a skip_initramfs function

I can't find examples of other devices where skip_initramfs is present in the dtb other than these so patch it out like we do the kernel

Co-authored-by: topjohnwu <topjohnwu@gmail.com>
2021-10-30 21:20:10 -07:00
Chris Renshaw
6663fd3526 Support custom legacy Sony devices with init.real setup
Custom ROM bring-ups of legacy Sony devices contain the following:
/init (symlink to /bin/init_sony)
/init.real (the "real" Android init)
/bin/init_sony (this was /sbin/init_sony on Android <11)

Kernel loads the ramdisk and starts /init -> /bin/init_sony
/bin/init_sony does low-level device setup (see: https://github.com/LineageOS/android_device_sony_common/blob/lineage-18.1/init/init_main.cpp)
/bin/init_sony unlinks /init and renames /init.real to /init
/bin/init_sony starts /init

Since init_sony needs to run first magiskinit needs to replace init.real instead, so add workarounds based on detection of init.real to boot patcher and uninstaller

Thanks @115ek and @bleckdeth

Fixes #3636

Co-authored-by: topjohnwu <topjohnwu@gmail.com>
2021-10-30 18:59:20 -07:00
topjohnwu
2c44e1bb93 Update rules again 2021-10-29 03:37:14 -07:00
残页
e3f6399473 Don't use xwrite() when patching legacy rootfs init
Fix topjohnwu#4810
> [    2.927463]  [1:           init:    1] magiskinit: Replace [/system/etc/selinux/plat_sepolicy.cil] -> [xxx]
[    2.936801]  [1:           init:    1] magiskinit: write failed with 14: Bad address

Since topjohnwu#4596, magisk fails to patch `/init`, xwrite() fails with EFAULT, break the original `/init` file and make the device unbootable. Reverting this commit for legacy rootfs devices fixes the problem. I think this is a Samsung kernel magic since currently I can't reproduce this on other devices or find something special in the log currently we have.
2021-10-29 03:23:34 -07:00
残页
89c2c21774 Fix init.rc path detection
Fix #4319
Some devices store init.rc into the new path but still have the legacy /init.rc file
2021-10-29 03:21:20 -07:00
vvb2060
2954eb4bdc Remove CustomTab 2021-10-29 03:17:11 -07:00
vvb2060
e08de91666 Clean sent proguard rules 2021-10-29 03:15:31 -07:00
残页
a170acb9d7 Fix compilation when init debug toggle enabled 2021-10-29 03:15:16 -07:00
vvb2060
6a086bb222 Load *_compat_cil_file from system_ext
https://android-review.googlesource.com/c/platform/system/core/+/1650271
2021-10-29 03:14:26 -07:00
vvb2060
b2f152e641 realpath /proc/pid/cwd
prevent cross mount namespace
2021-10-29 03:13:20 -07:00
topjohnwu
6c5b261804 Update spolicy rules 2021-10-29 03:12:48 -07:00
topjohnwu
8bd0c44e83 Replace module fd with memfd if possible 2021-10-28 00:26:18 -07:00
topjohnwu
34c36984e9 Stop extreme verbose logging 2021-10-27 04:00:40 -07:00
topjohnwu
8bd6aca0dd DenyList unmount without magiskd 2021-10-27 04:00:40 -07:00
topjohnwu
983b74be77 Pass MAGISKTMP over to zygote 2021-10-27 03:25:54 -07:00
topjohnwu
a3eafdd2c6 Release new canary build 2021-10-27 02:37:18 -07:00
topjohnwu
ea75a09f95 Make zygisk survive zygote restarts
Close #4777
2021-10-27 01:53:16 -07:00
LoveSy
4c747c4148 Add rule: allow * magisk_file lnk_file { * } 2021-10-26 00:41:04 -07:00
LoveSy
49abfcafed Fix nullptr dereference when env abnormal 2021-10-26 00:40:00 -07:00
topjohnwu
50710c72ad Cleanup magiskinit code 2021-10-26 00:35:55 -07:00
vvb2060
2e299b3814 Add an old cgroup v2 path
https://android-review.googlesource.com/c/platform/system/core/+/1324649
2021-10-25 20:54:19 -07:00
topjohnwu
43d11d877d Release new canary build 2021-10-24 00:22:13 -07:00
Arbri çoçka
d7e7df3bd9 Add translate Albania in stub 2021-10-23 23:55:57 -07:00
0purple
8d8ba11221 Update strings.xml 2021-10-23 23:55:38 -07:00
Ilya Kushnir
2536a18c00 Update RU strings 2021-10-23 23:55:09 -07:00
sn-o-w
11728b2b15 Update Romanian 2021-10-23 23:54:15 -07:00
green1052
627501b9ba Update Korean translation 2021-10-23 23:53:53 -07:00
vvb2060
3599384b38 Allow fallback to /dev/pts 2021-10-23 23:31:44 -07:00
topjohnwu
4b307cad2c Random minor changes 2021-10-23 22:20:07 -07:00
topjohnwu
7496d51580 Make zygiskd ABI aware 2021-10-23 14:38:30 -07:00
topjohnwu
4194ac894c Support setting more options 2021-10-21 03:20:04 -07:00
topjohnwu
ffb5d9ea9c Update libcxx 2021-10-21 02:08:13 -07:00
topjohnwu
770b28ca30 Build on API 21 headers 2021-10-20 03:17:42 -07:00
topjohnwu
62e464f706 Upgrade Android Studio 2021-10-20 01:43:33 -07:00
topjohnwu
8d0dc37ec0 Use SO_PEERSEC to get client secontext 2021-10-19 23:46:38 -07:00
topjohnwu
fe41df87bb pthread_cond_signal might wake multiple threads
Close #4759
2021-10-19 21:32:37 -07:00
topjohnwu
8276a0775d Fix API doc 2021-10-17 05:42:33 -07:00
LoveSy
abfb3bb3bb Fix always log hook fails 2021-10-17 05:00:31 -07:00
LoveSy
e184eb4a23 Fix UB of loading modules
- The lambda here infers its return type as `std::string`,
  and since `info` is `const`, the labmda copies `info.name`
  and returns a `std::string&&`. After captured by the
  `std::string_view`, the `std::string&&` return value
  deconstructs and makes `std::string_view` refers to a
  dangling pointer.
2021-10-17 04:38:56 -07:00
topjohnwu
d0fc372ecd Implement Zygisk companion process 2021-10-17 04:36:18 -07:00
topjohnwu
6f54c57647 Allow fork in thread pool 2021-10-17 04:24:25 -07:00
topjohnwu
e8ae103d5f Update jni hooks 2021-10-14 02:43:56 -07:00
topjohnwu
b0198dab6c Update Zygisk logging 2021-10-14 02:13:23 -07:00
topjohnwu
b75ec09998 Load Zygisk modules even if no magic mount is needed
Close #4767
2021-10-14 01:35:29 -07:00
topjohnwu
c8ac6c07b0 Load Zygisk modules 2021-10-13 04:52:02 -07:00
topjohnwu
27814e3015 Minor Zygisk API changes 2021-10-09 11:53:40 -07:00
topjohnwu
f59309a445 Minor changes 2021-10-09 11:36:01 -07:00
vvb2060
b0292d7319 Add execmem to allow hook 2021-10-09 02:59:03 -07:00
topjohnwu
7f18616cc0 Prune unused nodes from trie 2021-10-09 02:15:03 -07:00
topjohnwu
2fef98a5af Wipe out prop_info data after delete 2021-10-09 00:27:11 -07:00
topjohnwu
36765caedc Fix thread pool implementation
Close #4759
2021-10-08 23:28:14 -07:00
topjohnwu
f7aed10ea2 Fix friend template function 2021-10-08 19:17:31 -07:00
topjohnwu
410bbb8285 Update Zygisk APIs 2021-10-05 22:42:55 -07:00
topjohnwu
f56ea52932 Add public Zygisk API
Still WIP
2021-10-05 03:53:22 -07:00
vvb2060
cb4361b7b7 Fix terminal on android 8
No need to handle untrusted_app_all_devpts on modern devices, but devpts policy is different for older devices.
2021-09-25 12:08:35 -07:00
vvb2060
ecd332c573 Close fd 2021-09-25 12:07:52 -07:00
StoyanDimitrov
a0fe78a728 Update Bulgarian translation
Small fixes.
2021-09-24 01:30:37 -07:00
Aryan Sinha
49cc9c529e Magisk: values-hi: Update Hindi Translation
* Fix Some Grammatical Mistakes
* Simplify Some Words
2021-09-24 01:29:42 -07:00
Arbri çoçka
7635b2c33f Update and fix some bugs in values-sq 2021-09-24 01:28:51 -07:00
Ilya Kushnir
50c26d33ab Update RU strings 2021-09-24 01:28:30 -07:00
topjohnwu
f642fb3b99 Properly handle child zygote
Close #4720
2021-09-24 01:23:58 -07:00
topjohnwu
e68dd866a3 Only create app_id_map if necessary 2021-09-24 00:22:19 -07:00
topjohnwu
73d36fdff0 Riru and its modules are not compatible with zygisk 2021-09-23 23:54:46 -07:00
vvb2060
5561cd3c77 Update zh-rCN translation 2021-09-23 02:45:02 -07:00
usrDottik
32a9acb913 Updated values-es strings
Added DenyList and Zygisk translations
2021-09-23 02:44:47 -07:00
DanGLES3
f7f23c6e77 Update Brazilian Portugues translation 2021-09-23 02:44:29 -07:00
Arbri çoçka
3d4edbd9dc Update strings-sq 2021-09-23 02:43:45 -07:00
kubalav
bdf385f374 Update Slovak translation 2021-09-23 02:43:01 -07:00
Rom
9f78c3e64b Update French translation 2021-09-23 02:42:44 -07:00
taras
f370052815 Update Ukrainian translation 2021-09-23 02:42:20 -07:00
Oliver Cervera
9df4b10067 Update Italian translation 2021-09-23 02:41:53 -07:00
vvb2060
d20517483e Prevent multiple mounts of devpts 2021-09-23 02:40:24 -07:00
Thonsi
713ce4719b Cleanup unused code 2021-09-23 02:39:48 -07:00
topjohnwu
f3d39e7515 Update BusyBox
Fix #4657, close #4602
2021-09-23 02:31:42 -07:00
残页
61783ffc82 Prevent original libselinux.so to be unmounted
libselinux.so will be unmounted when magiskd starts. If magiskd restarts (like it died before boot completed), the files we want to unmount is the original files because the modified files is unmounted in previous start, which will causes many crashes due to missing libselinux.so.
2021-09-22 19:34:44 -07:00
topjohnwu
05c4ad01d5 Move first stage unload into second stage 2021-09-22 19:33:08 -07:00
topjohnwu
12647dcf30 Improve memory map tricks
- In `unmap_all`, replace readable pages atomically with mmap + mremap
- Create new function `remap_all` to replace pages with equivalent
  anonymous copies to prevent simple maps name scanning
2021-09-22 19:14:05 -07:00
topjohnwu
da38f59e62 Only run destructor if necessary 2021-09-22 02:52:33 -07:00
topjohnwu
cf4ef54dc5 Unload first stage on main thread 2021-09-22 02:46:07 -07:00
topjohnwu
12e9873514 Update zygisk entry implementation 2021-09-22 00:14:22 -07:00
RikkaW
f7c0e407ca Fix downgrade database 2021-09-20 05:50:34 -07:00
topjohnwu
82c7662cdf Cache Magisk app ID for performance 2021-09-20 05:47:15 -07:00
topjohnwu
4f0bced53e Track app ID instead of UID 2021-09-20 05:08:25 -07:00
topjohnwu
f1b6c9f4aa Refresh uid_map on package.xml change 2021-09-20 04:42:06 -07:00
topjohnwu
0ab31ab0df Fix log writer 2021-09-19 13:41:45 -07:00
topjohnwu
46e8f0779f Move denylist code into zygisk 2021-09-18 14:50:11 -07:00
topjohnwu
3fb72a4d20 Support polling on multiple fds 2021-09-18 14:40:12 -07:00
topjohnwu
db20f65d7c On denylist != do unmount 2021-09-18 12:44:42 -07:00
topjohnwu
63cfe7b47b Make sanitize_environ work properly 2021-09-18 05:11:10 -07:00
topjohnwu
db590091b3 Propagate Zygisk state to Magisk app 2021-09-18 02:38:53 -07:00
topjohnwu
7b25e74418 Simplify get manager app info logic 2021-09-17 02:07:32 -07:00
vvb2060
82f303e1c6 Allow save app log when not activated
may be useful for patch boot.
2021-09-16 19:44:45 -07:00
Vladimír Kubala
c038683b54 Update Slovak translation 2021-09-16 19:41:43 -07:00
vvb2060
3a37ed6b60 Update zh-rCN translation 2021-09-16 19:41:19 -07:00
topjohnwu
706a492218 Update denylist config implementation 2021-09-16 05:27:34 -07:00
topjohnwu
c0be5383de Support enable/disable Zygisk 2021-09-15 02:49:54 -07:00
topjohnwu
3b8ce85092 Enable Zygisk 2021-09-15 01:59:43 -07:00
topjohnwu
b6298f8602 Remove more code 2021-09-13 02:00:04 -07:00
topjohnwu
abfec57972 Move files around 2021-09-13 01:47:36 -07:00
topjohnwu
470fc97d1f Remove SafetyNet check 2021-09-13 01:41:31 -07:00
topjohnwu
8d59caf635 Cleanup unused code 2021-09-13 01:06:43 -07:00
topjohnwu
acf25aa4d3 Remove DoH 2021-09-13 00:44:49 -07:00
topjohnwu
16de4674ec Move denylist fragment to settings 2021-09-13 00:33:36 -07:00
topjohnwu
65b0ea792e MagiskHide is no more 2021-09-12 12:40:34 -07:00
topjohnwu
fc6b02f607 Move denylist fragment to its own section 2021-09-12 00:39:24 -07:00
topjohnwu
136d8c39d9 Move more code into buildSrc 2021-09-09 20:19:49 -07:00
topjohnwu
24a8b41182 Fix indentation 2021-09-09 01:37:49 -07:00
vvb2060
810cf4dee8 Move config to buildSrc 2021-09-09 01:37:20 -07:00
LoveSy
9bf835e810 Fix ccache 2021-09-08 08:53:51 -07:00
topjohnwu
eca37bce38 Separate dependency and build cache 2021-09-08 01:16:26 -07:00
topjohnwu
3ee6a2baf2 Enable ccache for faster builds 2021-09-08 01:13:36 -07:00
topjohnwu
69fa7f238d Don't cache NDK 2021-09-08 01:13:36 -07:00
topjohnwu
de2306bd12 Proper incremental builds
Auto generate flag.h for precise rebuilding
2021-09-07 19:35:28 -07:00
topjohnwu
714feeb9a7 Support building on case insensitive filesystems
Fix #4643, close #4641
2021-09-07 04:02:29 -07:00
topjohnwu
ca99808fd2 Update AVD support
- Support arm64 AVD images
- Support setup on Windows

 Close #4637
2021-09-07 03:03:02 -07:00
topjohnwu
f8f8c28fec Switch zopfli to official repo 2021-09-03 10:39:23 -07:00
vvb2060
f497867ba5 Update submodules 2021-09-03 10:24:33 -07:00
RikkaW
383192784d Use standard BottomNav & Remove hide on scroll for AppBar and BottomNav 2021-09-03 10:22:46 -07:00
vvb2060
605189bc6e Hide overlay windows 2021-09-02 23:17:01 -07:00
残页
c0a2e3674c Reset file context from adb_data_file
In some cases (like weird ROMs that allow init to relabel context from system_file), module files will have an incorrent context, which will causes module not working properly.
See https://github.com/RikkaApps/Riru/wiki/Explanation-about-incorrect-SELinux-rules-from-third-party-ROMs-cause-Riru-not-working
2021-09-02 21:55:08 -07:00
vvb2060
76f0602684 Make busybox cflag stable 2021-09-02 21:48:38 -07:00
vvb2060
477ff12cde print sepolicy rules dir 2021-09-02 21:46:43 -07:00
topjohnwu
9c09ad3b62 Open source fully obfuscated stub 2021-09-02 21:31:33 -07:00
topjohnwu
a967afc629 Update macOS JAVA_HOME path 2021-09-02 02:27:05 -07:00
vvb2060
dcc1fd3ee4 Use PACKAGE_FULLY_REMOVED for magiskhide
Remove from magiskhide only if pkg fully deleted
2021-09-02 02:15:25 -07:00
vvb2060
933f020b3c Show dialog when hide or restore app 2021-09-02 02:13:31 -07:00
vvb2060
f5c02be5bf Add new targetSdk domain
https://android-review.googlesource.com/c/platform/system/sepolicy/+/1752122
2021-09-02 01:48:25 -07:00
vvb2060
68fbdd474c Target SDK 31 2021-09-02 01:39:34 -07:00
vvb2060
2cbc048352 Add mount info to log file 2021-09-02 00:48:44 -07:00
Wang Han
e990ffd4a0 Remove leftover DISABLE_ZYGISK flag 2021-09-01 20:14:53 -07:00
topjohnwu
743c7c9326 App code reorganization 2021-09-01 01:17:27 -07:00
topjohnwu
067248da75 Cleanup RvItems 2021-09-01 01:17:27 -07:00
topjohnwu
f5c982355a Remove online section in modules fragment 2021-09-01 01:17:27 -07:00
vvb2060
f98c68a280 Clean up unneeded rules 2021-08-29 13:03:50 -07:00
vvb2060
773bf0c6bc Do not use glob in the system's unzip parameter
https://android.googlesource.com/platform/system/core/+/refs/tags/android-10.0.0_r47/libziparchive/unzip.cpp#57 unzip did not support glob before Android 11
2021-08-29 13:03:41 -07:00
Arbri çoçka
080ab6032c update and fix some text in strings-sq 2021-08-29 11:36:07 -07:00
vvb2060
350144df29 Do not allow remove the module to be updated 2021-08-29 11:35:46 -07:00
Antikruk
9ac0f11d9a Update Belarusian translation 2021-08-29 03:27:57 -07:00
LoveSy
8079d456ab Use std::map instead 2021-08-29 03:27:15 -07:00
vvb2060
acf166cf9d Support oplus.fstab 2021-08-29 03:27:15 -07:00
vvb2060
439d497a13 boot image header v4 2021-08-29 03:14:23 -07:00
Allan Nordhøy
0580932610 Norwegian Bokmål translation 2021-08-29 03:10:39 -07:00
Arbri çoçka
85399f609c Fix and update strings-sq 2021-08-29 03:10:01 -07:00
LoveSy
4bcfee397b Remove unnecessary umount 2021-08-29 02:45:49 -07:00
vvb2060
34bcb1dd26 Fix line editing on Android 8.0+ 2021-08-29 02:45:49 -07:00
LoveSy
117d1ed080 Fix always enter safe mode
`getprop("persist.sys.safemode", true) == "1"` -> `getprop("persist.sys.safemode", true) == ""`
2021-08-29 02:45:49 -07:00
vvb2060
f324252681 Use isolated devpts if kernel support
kernel version >= 4.7 or CONFIG_DEVPTS_MULTIPLE_INSTANCES=y
2021-08-29 02:45:49 -07:00
LoveSy
0dad06cdfe Fix meizu compatibility 2021-08-28 21:03:12 -07:00
vvb2060
9396288ca2 Check util_functions.sh version 2021-08-28 20:52:05 -07:00
LoveSy
f89f08833e Generic parsing methods 2021-08-28 20:50:17 -07:00
vvb2060
79e8962854 Support bootconfig
https://android-review.googlesource.com/c/platform/system/core/+/1615298
2021-08-28 20:50:17 -07:00
topjohnwu
34e5a7cd24 Zopfli is not always smaller 2021-08-28 17:16:20 -07:00
topjohnwu
7343c195b7 Cleanup compress.cpp 2021-08-28 17:01:08 -07:00
topjohnwu
0af041b54e Cleanup magiskboot code 2021-08-28 14:07:34 -07:00
Chaosmaster
92a8a3e91f Add zopfli gzip encoder for better compression 2021-08-28 11:00:30 -07:00
Chaosmaster
f41575d8b0 Add support for legacy ARM32 zImage 2021-08-28 10:53:45 -07:00
topjohnwu
d93c4a5103 Update README 2021-08-28 10:45:32 -07:00
topjohnwu
6fe9b69aad Cleanup module.cpp 2021-08-28 10:27:45 -07:00
topjohnwu
5d162f81c4 Modernize db.hpp 2021-08-27 01:06:03 -07:00
topjohnwu
4771c2810b Significantly better AVD support 2021-08-26 03:09:56 -07:00
topjohnwu
0cd99712fa Implement cached thread pool 2021-08-24 02:39:54 -07:00
topjohnwu
b591af7803 Minor bug fixes 2021-08-22 03:26:48 -07:00
topjohnwu
171d68ca72 Connect to magiskd log daemon 2021-08-22 03:26:48 -07:00
topjohnwu
bade4f2c6a Make xhook log as Magisk 2021-08-22 03:26:48 -07:00
topjohnwu
5754782a4e Generalize gen_jni_hooks.py 2021-08-22 03:26:48 -07:00
topjohnwu
decdd54c19 Hook up denylist IPC routines 2021-08-22 03:26:48 -07:00
topjohnwu
ffe47300a1 Update recv/send fd function 2021-08-22 03:26:48 -07:00
topjohnwu
6f9c3c4ff3 Refactor hook.cpp 2021-08-19 01:54:12 -07:00
topjohnwu
9b3efffba9 Use magiskd to setup files 2021-08-18 03:44:32 -07:00
topjohnwu
003fea52b1 Remove all non-Magisk hiding code
Magisk no longer interferes with any signals/info that were not created
or caused by Magisk itself.
2021-08-18 02:01:54 -07:00
topjohnwu
2b17c77195 Make Zygisk 1st class citizen 2021-08-17 23:57:49 -07:00
topjohnwu
c252a50fd7 The name is Zygisk 2021-08-17 23:38:40 -07:00
topjohnwu
cf8f042a20 Cleanup magiskboot cpio code 2021-08-13 04:53:11 -07:00
topjohnwu
844bc2d808 Remove unused code 2021-08-13 03:30:58 -07:00
topjohnwu
27f7fa7153 Extend stream support 2021-08-13 02:08:56 -07:00
topjohnwu
b325aa4555 Fix log file writing 2021-08-13 00:13:44 -07:00
topjohnwu
c2c3bf0ba4 Don't depend on vtable ABI layout 2021-08-12 06:41:59 -07:00
topjohnwu
0d977b54f7 Revise logging code 2021-08-12 03:26:54 -07:00
topjohnwu
20860da4b4 Cleaner daemon handlers 2021-08-11 22:57:08 -07:00
topjohnwu
3ea10b7cf9 Reorganize injection code 2021-08-11 22:56:18 -07:00
topjohnwu
1ec33863bc Android 5.0 is actually supported 2021-08-11 17:14:22 -07:00
topjohnwu
a260e99090 Support code injection on Android 12 2021-08-11 00:00:21 -07:00
topjohnwu
25efdd3d6f Use code generator for jni_hooks 2021-08-02 03:20:19 -07:00
topjohnwu
00a1e18959 Store all native JNI methods in data structures 2021-08-01 14:35:16 -07:00
topjohnwu
c59f8adc4a Update Android Studio 2021-07-30 14:23:20 -07:00
topjohnwu
1eb83ad812 Update Android Studio 2021-05-16 01:26:54 -07:00
topjohnwu
7717f0a6b0 Support Android S AVD 2021-05-13 04:45:13 -07:00
topjohnwu
5e1fba3603 Build a single APK for all ABIs 2021-05-13 00:21:04 -07:00
vvb2060
66cc9bc545 Pure 64bit support 2021-05-12 16:38:34 -07:00
vvb2060
12aa5838d9 Stop gradle daemon 2021-05-12 16:38:34 -07:00
topjohnwu
4f73534837 Update installation instructions 2021-05-12 02:14:41 -07:00
topjohnwu
c4d145835c Release new canary build 2021-05-11 22:40:40 -07:00
topjohnwu
f822ca5b23 Update changelogs 2021-05-11 22:31:12 -07:00
topjohnwu
8aaa45c62a Release Magisk v23.0 2021-05-11 22:15:52 -07:00
topjohnwu
2f4f257070 Publish v23.0 docs 2021-05-11 22:08:02 -07:00
471 changed files with 12785 additions and 14610 deletions

19
.github/ccache.sh vendored Normal file
View File

@@ -0,0 +1,19 @@
OS=$(uname)
CCACHE_VER=4.4
case $OS in
Darwin )
brew install ccache
ln -s $(which ccache) ./ccache
;;
Linux )
sudo apt-get install -y ccache
ln -s $(which ccache) ./ccache
;;
* )
curl -OL https://github.com/ccache/ccache/releases/download/v${CCACHE_VER}/ccache-${CCACHE_VER}-windows-64.zip
unzip -j ccache-*-windows-64.zip '*/ccache.exe'
;;
esac
mkdir ./.ccache
./ccache -o compiler_check='%compiler% -dumpmachine; %compiler% -dumpversion'

View File

@@ -10,6 +10,7 @@ on:
- 'buildSrc/**' - 'buildSrc/**'
- 'build.py' - 'build.py'
- 'gradle.properties' - 'gradle.properties'
- '.github/workflows/build.yml'
pull_request: pull_request:
branches: [ master ] branches: [ master ]
workflow_dispatch: workflow_dispatch:
@@ -21,7 +22,10 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ ubuntu-latest, windows-latest, macOS-latest ] os: [ ubuntu-latest, windows-latest, macos-latest ]
env:
NDK_CCACHE: ${{ github.workspace }}/ccache
CCACHE_DIR: ${{ github.workspace }}/.ccache
steps: steps:
- name: Check out - name: Check out
@@ -40,50 +44,43 @@ jobs:
with: with:
python-version: '3.x' python-version: '3.x'
- name: Set up GitHub env (Windows) - name: Set up ccache
if: runner.os == 'Windows' run: bash .github/ccache.sh
run: |
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
echo "ANDROID_SDK_ROOT=$env:ANDROID_SDK_ROOT" >> $env:GITHUB_ENV
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $env:GITHUB_ENV
echo "GRADLE_OPTS=-Dorg.gradle.daemon=false" >> $env:GITHUB_ENV
- name: Set up GitHub env (Unix) - name: Cache Gradle dependencies
if: runner.os != 'Windows'
run: |
ndk_ver=$(sed -n 's/^magisk.fullNdkVersion=//p' gradle.properties)
echo ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT >> $GITHUB_ENV
echo MAGISK_NDK_VERSION=$ndk_ver >> $GITHUB_ENV
- name: Cache Gradle
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
~/.gradle/wrapper ~/.gradle/wrapper
!~/.gradle/caches/build-cache-*
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }} key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle- restore-keys: ${{ runner.os }}-gradle-
- name: Cache NDK - name: Cache build cache
id: ndk-cache
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk path: |
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }} ${{ github.workspace }}/.ccache
~/.gradle/caches/build-cache-*
key: ${{ runner.os }}-build-cache-${{ github.sha }}
restore-keys: ${{ runner.os }}-build-cache-
- name: Set up NDK - name: Set up NDK
if: steps.ndk-cache.outputs.cache-hit != 'true' run: python build.py -v ndk
run: python build.py ndk
- name: Build release - name: Build release
run: python build.py -vr all run: |
./ccache -zp
- name: Refresh flag python build.py -vr all
run: touch gradle.properties
shell: bash
- name: Build debug - name: Build debug
run: python build.py -v all run: |
python build.py -v all
./ccache -s
- name: Stop gradle daemon
run: ./gradlew --stop
# Only upload artifacts built on Linux # Only upload artifacts built on Linux
- name: Upload build artifact - name: Upload build artifact

View File

@@ -1,26 +0,0 @@
name: Check Issues
on:
issues:
types: [opened]
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Check out
uses: actions/checkout@v2
- name: Read latest version code
run: |
ver=$(sed -n 's/^magisk.versionCode=//p' gradle.properties)
echo MAGISK_VERSION_CODE=$ver >> $GITHUB_ENV
- if: contains(github.event.issue.body, format('Magisk version code{0} ', ':')) != true
id: close
name: Close Issue(template)
uses: peter-evans/close-issue@v1
with:
comment: This issue is being automatically closed because it does not follow the issue template.
- if: steps.close.conclusion == 'skipped' && contains(github.event.issue.body, format('Magisk version code{0} {1}', ':', env.MAGISK_VERSION_CODE)) != true
name: Close Issue(latest canary)
uses: peter-evans/close-issue@v1
with:
comment: This issue is being automatically closed because latest canary Magisk version code is ${{ env.MAGISK_VERSION_CODE }}.

1
.gitignore vendored
View File

@@ -4,6 +4,7 @@ out
*.apk *.apk
/config.prop /config.prop
/update.sh /update.sh
/dict.txt
# Built binaries # Built binaries
native/out native/out

6
.gitmodules vendored
View File

@@ -34,6 +34,12 @@
[submodule "zlib"] [submodule "zlib"]
path = native/jni/external/zlib path = native/jni/external/zlib
url = https://android.googlesource.com/platform/external/zlib url = https://android.googlesource.com/platform/external/zlib
[submodule "parallel-hashmap"]
path = native/jni/external/parallel-hashmap
url = https://github.com/greg7mdp/parallel-hashmap.git
[submodule "termux-elf-cleaner"] [submodule "termux-elf-cleaner"]
path = tools/termux-elf-cleaner path = tools/termux-elf-cleaner
url = https://github.com/termux/termux-elf-cleaner.git url = https://github.com/termux/termux-elf-cleaner.git
[submodule "zopfli"]
path = native/jni/external/zopfli
url = https://github.com/google/zopfli.git

View File

@@ -2,28 +2,29 @@
[![Downloads](https://img.shields.io/badge/dynamic/json?color=green&label=Downloads&query=totalString&url=https%3A%2F%2Fraw.githubusercontent.com%2Ftopjohnwu%2Fmagisk-files%2Fcount%2Fcount.json&cacheSeconds=1800)](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json) [![Downloads](https://img.shields.io/badge/dynamic/json?color=green&label=Downloads&query=totalString&url=https%3A%2F%2Fraw.githubusercontent.com%2Ftopjohnwu%2Fmagisk-files%2Fcount%2Fcount.json&cacheSeconds=1800)](https://raw.githubusercontent.com/topjohnwu/magisk-files/count/count.json)
#### This is not an officially supported Google product
## Introduction ## Introduction
Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br> Magisk is a suite of open source software for customizing Android, supporting devices higher than Android 5.0.<br>
Here are some feature highlights: Some highlight features:
- **MagiskSU**: Provide root access for applications - **MagiskSU**: Provide root access for applications
- **Magisk Modules**: Modify read-only partitions by installing modules - **Magisk Modules**: Modify read-only partitions by installing modules
- **MagiskHide**: Hide Magisk from root detections / system integrity checks
- **MagiskBoot**: The most complete tool for unpacking and repacking Android boot images - **MagiskBoot**: The most complete tool for unpacking and repacking Android boot images
- **Zygisk**: Run code in every Android applications' processes
## Downloads ## Downloads
[Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads. [Github](https://github.com/topjohnwu/Magisk/) is the only source where you can get official Magisk information and downloads.
[![](https://img.shields.io/badge/Magisk-v22.1-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v22.1) [![](https://img.shields.io/badge/Magisk-v23.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v23.0)
[![](https://img.shields.io/badge/Magisk%20Beta-v22.1-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v22.1) [![](https://img.shields.io/badge/Magisk%20Beta-v24.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v24.0)
[![](https://img.shields.io/badge/Magisk-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk) [![](https://img.shields.io/badge/Magisk-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk-files/canary/app-debug.apk)
## Useful Links ## Useful Links
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html) - [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
- [Frequently Asked Questions](https://topjohnwu.github.io/Magisk/faq.html)
- [Magisk Documentation](https://topjohnwu.github.io/Magisk/) - [Magisk Documentation](https://topjohnwu.github.io/Magisk/)
- [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan)) - [Magisk Troubleshoot Wiki](https://www.didgeridoohan.com/magisk/HomePage) (by [@Didgeridoohan](https://github.com/Didgeridoohan))
@@ -40,15 +41,15 @@ For Magisk app crashes, record and upload the logcat when the crash occurs.
- Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups. - Magisk builds on any OS Android Studio supports. Install Android Studio and do the initial setups.
- Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git` - Clone sources: `git clone --recurse-submodules https://github.com/topjohnwu/Magisk.git`
- Install Python 3.6+ \ - Install Python 3.6+ \
(Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install) (Windows only: select **'Add Python to PATH'** in installer, and run `pip install colorama` after install)
- Configure to use the JDK bundled in Android Studio: - Configure to use the JDK bundled in Android Studio:
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"` - macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/Contents/Home"`
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"` - Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH` - Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings) - Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
- Run `./build.py ndk` to let the script download and install NDK for you - Run `./build.py ndk` to let the script download and install NDK for you
- To start building, run `build.py` to see your options. \ - To start building, run `build.py` to see your options. \
For each action, use `-h` to access help (e.g. `./build.py all -h`) For each action, use `-h` to access help (e.g. `./build.py all -h`)
- To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources. - To start development, open the project with Android Studio. The IDE can be used for both app (Kotlin/Java) and native (C++/C) sources.
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided. - Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key). - To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).

View File

@@ -1,6 +1,3 @@
import org.apache.tools.ant.filters.FixCrLfFilter
import java.io.PrintStream
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
@@ -27,17 +24,14 @@ android {
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
versionName = Config.version versionName = Config.version
versionCode = Config.versionCode versionCode = Config.versionCode
ndk.abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64") ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
} }
buildTypes { buildTypes {
getByName("release") { release {
isMinifyEnabled = true isMinifyEnabled = true
isShrinkResources = true isShrinkResources = true
proguardFiles( proguardFiles("proguard-rules.pro")
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
} }
} }
@@ -45,204 +39,90 @@ android {
dataBinding = true dataBinding = true
} }
dependenciesInfo {
includeInApk = false
includeInBundle = false
}
packagingOptions { packagingOptions {
exclude("/META-INF/*") resources {
exclude("/org/bouncycastle/**") excludes += "/META-INF/*"
exclude("/kotlin/**") excludes += "/org/bouncycastle/**"
exclude("/kotlinx/**") excludes += "/kotlin/**"
exclude("/okhttp3/**") excludes += "/kotlinx/**"
exclude("/*.txt") excludes += "/okhttp3/**"
exclude("/*.bin") excludes += "/*.txt"
doNotStrip("**/*.so") excludes += "/*.bin"
excludes += "/*.json"
}
jniLibs {
keepDebugSymbols += "**/*.so"
}
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = "11"
freeCompilerArgs = listOf("-Xjvm-default=enable")
} }
} }
val syncLibs by tasks.registering(Sync::class) { setupApp()
into("src/main/jniLibs")
into("armeabi-v7a") {
from(rootProject.file("native/out/armeabi-v7a")) {
include("busybox", "magiskboot", "magiskinit", "magisk")
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
}
from(rootProject.file("native/out/arm64-v8a")) {
include("magisk")
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
}
}
into("x86") {
from(rootProject.file("native/out/x86")) {
include("busybox", "magiskboot", "magiskinit", "magisk")
rename { if (it == "magisk") "libmagisk32.so" else "lib$it.so" }
}
from(rootProject.file("native/out/x86_64")) {
include("magisk")
rename { if (it == "magisk") "libmagisk64.so" else "lib$it.so" }
}
}
onlyIf {
if (inputs.sourceFiles.files.size != 10)
throw StopExecutionException("Please build binaries first! (./build.py binary)")
true
}
}
val createStubLibs by tasks.registering { configurations.all {
dependsOn(syncLibs) exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk7")
doLast { exclude("org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
val arm64 = project.file("src/main/jniLibs/arm64-v8a/libstub.so")
arm64.parentFile.mkdirs()
arm64.createNewFile()
val x64 = project.file("src/main/jniLibs/x86_64/libstub.so")
x64.parentFile.mkdirs()
x64.createNewFile()
}
}
val syncAssets by tasks.registering(Sync::class) {
dependsOn(createStubLibs)
inputs.property("version", Config.version)
inputs.property("versionCode", Config.versionCode)
into("src/main/assets")
from(rootProject.file("scripts")) {
include("util_functions.sh", "boot_patch.sh", "uninstaller.sh", "addon.d.sh")
}
into("chromeos") {
from(rootProject.file("tools/futility"))
from(rootProject.file("tools/keys")) {
include("kernel_data_key.vbprivk", "kernel.keyblock")
}
}
filesMatching("**/util_functions.sh") {
filter {
it.replace("#MAGISK_VERSION_STUB",
"MAGISK_VER='${Config.version}'\n" +
"MAGISK_VER_CODE=${Config.versionCode}")
}
filter<FixCrLfFilter>("eol" to FixCrLfFilter.CrLf.newInstance("lf"))
}
}
val syncResources by tasks.registering(Sync::class) {
dependsOn(syncAssets)
into("src/main/resources/META-INF/com/google/android")
from(rootProject.file("scripts/update_binary.sh")) {
rename { "update-binary" }
}
from(rootProject.file("scripts/flash_script.sh")) {
rename { "updater-script" }
}
}
tasks["preBuild"]?.dependsOn(syncResources)
android.applicationVariants.all {
val keysDir = rootProject.file("tools/keys")
val outSrcDir = File(buildDir, "generated/source/keydata/$name")
val outSrc = File(outSrcDir, "com/topjohnwu/signing/KeyData.java")
fun PrintStream.newField(name: String, file: File) {
println("public static byte[] $name() {")
print("byte[] buf = {")
val bytes = file.readBytes()
print(bytes.joinToString(",") { "(byte)(${it.toInt() and 0xff})" })
println("};")
println("return buf;")
println("}")
}
val genSrcTask = tasks.register("generate${name.capitalize()}KeyData") {
inputs.dir(keysDir)
outputs.file(outSrc)
doLast {
outSrc.parentFile.mkdirs()
PrintStream(outSrc).use {
it.println("package com.topjohnwu.signing;")
it.println("public final class KeyData {")
it.newField("testCert", File(keysDir, "testkey.x509.pem"))
it.newField("testKey", File(keysDir, "testkey.pk8"))
it.newField("verityCert", File(keysDir, "verity.x509.pem"))
it.newField("verityKey", File(keysDir, "verity.pk8"))
it.println("}")
}
}
}
registerJavaGeneratingTask(genSrcTask.get(), outSrcDir)
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(kotlin("stdlib"))
// Some dependencies request JDK 8 stdlib, specify manually here to prevent version mismatch
implementation(kotlin("stdlib-jdk8"))
implementation(project(":app:shared")) implementation(project(":app:shared"))
implementation("com.github.topjohnwu:jtar:1.0.0") implementation("com.github.topjohnwu:jtar:1.0.0")
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7") implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
implementation("com.github.topjohnwu:lz4-java:1.7.1") implementation("com.github.topjohnwu:lz4-java:1.7.1")
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
val vBC = "1.68" implementation("dev.rikka.rikkax.layoutinflater:layoutinflater:1.2.0")
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}") implementation("dev.rikka.rikkax.insets:insets:1.1.1")
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}") implementation("dev.rikka.rikkax.recyclerview:recyclerview-ktx:1.3.1")
implementation("io.noties.markwon:core:4.6.2")
val vBAdapt = "4.0.0" val vBAdapt = "4.0.0"
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter" val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
implementation("${bindingAdapter}:${vBAdapt}") implementation("${bindingAdapter}:${vBAdapt}")
implementation("${bindingAdapter}-recyclerview:${vBAdapt}") implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
val vMarkwon = "4.6.2" val vLibsu = "3.2.1"
implementation("io.noties.markwon:core:${vMarkwon}")
implementation("io.noties.markwon:html:${vMarkwon}")
implementation("io.noties.markwon:image:${vMarkwon}")
implementation("com.caverock:androidsvg:1.4")
val vLibsu = "3.1.2"
implementation("com.github.topjohnwu.libsu:core:${vLibsu}") implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
implementation("com.github.topjohnwu.libsu:io:${vLibsu}") implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
implementation("com.github.topjohnwu.libsu:service:${vLibsu}")
val vRetrofit = "2.9.0" val vRetrofit = "2.9.0"
implementation("com.squareup.retrofit2:retrofit:${vRetrofit}") implementation("com.squareup.retrofit2:retrofit:${vRetrofit}")
implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}") implementation("com.squareup.retrofit2:converter-moshi:${vRetrofit}")
implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}") implementation("com.squareup.retrofit2:converter-scalars:${vRetrofit}")
val vOkHttp = "4.9.1" val vOkHttp = "4.9.3"
implementation("com.squareup.okhttp3:okhttp:${vOkHttp}") implementation("com.squareup.okhttp3:okhttp:${vOkHttp}")
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}") implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}") implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
val vMoshi = "1.12.0" val vMoshi = "1.13.0"
implementation("com.squareup.moshi:moshi:${vMoshi}") implementation("com.squareup.moshi:moshi:${vMoshi}")
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}") kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
val vRoom = "2.3.0" val vRoom = "2.4.1"
implementation("androidx.room:room-runtime:${vRoom}") implementation("androidx.room:room-runtime:${vRoom}")
implementation("androidx.room:room-ktx:${vRoom}") implementation("androidx.room:room-ktx:${vRoom}")
kapt("androidx.room:room-compiler:${vRoom}") kapt("androidx.room:room-compiler:${vRoom}")
val vNav: String by rootProject.extra val vNav = "2.5.0-alpha01"
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}") implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
implementation("androidx.navigation:navigation-ui-ktx:${vNav}") implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
implementation("androidx.biometric:biometric:1.1.0") implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.4") implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.browser:browser:1.3.0") implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.preference:preference:1.1.1") implementation("androidx.preference:preference:1.2.0")
implementation("androidx.recyclerview:recyclerview:1.2.0") implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.fragment:fragment-ktx:1.3.3") implementation("androidx.fragment:fragment-ktx:1.4.1")
implementation("androidx.work:work-runtime-ktx:2.5.0")
implementation("androidx.transition:transition:1.4.1") implementation("androidx.transition:transition:1.4.1")
implementation("androidx.core:core-ktx:1.3.2") implementation("androidx.core:core-ktx:1.7.0")
implementation("com.google.android.material:material:1.3.0") implementation("androidx.core:core-splashscreen:1.0.0-beta01")
implementation("com.google.android.material:material:1.5.0")
} }

View File

@@ -16,17 +16,17 @@
# public *; # public *;
#} #}
# Parcelable
-keepclassmembers class * implements android.os.Parcelable {
public static final ** CREATOR;
}
# Kotlin # Kotlin
-assumenosideeffects class kotlin.jvm.internal.Intrinsics { -assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void check*(...); public static void check*(...);
public static void throw*(...); public static void throw*(...);
} }
# Snet
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback { *; }
# Stub # Stub
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); } -keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
-keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl { -keepclassmembers class androidx.appcompat.app.AppCompatDelegateImpl {
@@ -44,6 +44,10 @@
-repackageclasses 'a' -repackageclasses 'a'
-allowaccessmodification -allowaccessmodification
-obfuscationdictionary ../dict.txt
-classobfuscationdictionary ../dict.txt
-packageobfuscationdictionary ../dict.txt
-dontwarn org.bouncycastle.jsse.BCSSLParameters -dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket -dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider -dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider

View File

@@ -2,13 +2,14 @@ plugins {
id("com.android.library") id("com.android.library")
} }
setupCommon()
android { android {
defaultConfig { defaultConfig {
vectorDrawables.useSupportLibrary = true
consumerProguardFiles("proguard-rules.pro") consumerProguardFiles("proguard-rules.pro")
} }
} }
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) api("io.michaelrocks:paranoid-core:0.3.7")
} }

View File

@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute" />
</manifest>

View File

@@ -7,6 +7,8 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
<uses-permission android:name="android.permission.UPDATE_PACKAGES_WITHOUT_USER_ACTION" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" android:maxSdkVersion="29"
@@ -20,8 +22,6 @@
android:label="Magisk" android:label="Magisk"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute" />
</manifest> </manifest>

View File

@@ -1,5 +1,7 @@
package com.topjohnwu.magisk; package com.topjohnwu.magisk;
import static android.os.Build.VERSION.SDK_INT;
import android.content.Context; import android.content.Context;
import android.content.res.AssetManager; import android.content.res.AssetManager;
@@ -7,14 +9,10 @@ import java.io.File;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Map; import java.util.Map;
import static android.os.Build.VERSION.SDK_INT; import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate
public class DynAPK { public class DynAPK {
// Indices of the object array
private static final int STUB_VERSION_ENTRY = 0;
private static final int CLASS_COMPONENT_MAP = 1;
private static File dynDir; private static File dynDir;
private static Method addAssetPath; private static Method addAssetPath;
@@ -38,21 +36,6 @@ public class DynAPK {
return new File(getDynDir(c), "update.apk"); return new File(getDynDir(c), "update.apk");
} }
public static Data load(Object o) {
Object[] arr = (Object[]) o;
Data data = new Data();
data.version = (int) arr[STUB_VERSION_ENTRY];
data.classToComponent = (Map<String, String>) arr[CLASS_COMPONENT_MAP];
return data;
}
public static Object pack(Data data) {
Object[] arr = new Object[2];
arr[STUB_VERSION_ENTRY] = data.version;
arr[CLASS_COMPONENT_MAP] = data.classToComponent;
return arr;
}
public static void addAssetPath(AssetManager asset, String path) { public static void addAssetPath(AssetManager asset, String path) {
try { try {
if (addAssetPath == null) if (addAssetPath == null)
@@ -62,7 +45,28 @@ public class DynAPK {
} }
public static class Data { public static class Data {
public int version; // Indices of the object array
public Map<String, String> classToComponent; private static final int STUB_VERSION = 0;
private static final int CLASS_COMPONENT_MAP = 1;
private static final int ROOT_SERVICE = 2;
private static final int ARR_SIZE = 3;
private final Object[] arr;
public Data() { arr = new Object[ARR_SIZE]; }
public Data(Object o) { arr = (Object[]) o; }
public Object getObject() { return arr; }
public int getVersion() { return (int) arr[STUB_VERSION]; }
public void setVersion(int version) { arr[STUB_VERSION] = version; }
public Map<String, String> getClassToComponent() {
// noinspection unchecked
return (Map<String, String>) arr[CLASS_COMPONENT_MAP];
}
public void setClassToComponent(Map<String, String> map) {
arr[CLASS_COMPONENT_MAP] = map;
}
public Class<?> getRootService() { return (Class<?>) arr[ROOT_SERVICE]; }
public void setRootService(Class<?> service) { arr[ROOT_SERVICE] = service; }
} }
} }

View File

@@ -1,300 +0,0 @@
package com.topjohnwu.magisk;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Modified from androidx.core.content.FileProvider
*/
public class FileProvider extends ContentProvider {
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
private static final File DEVICE_ROOT = new File("/");
private static final HashMap<String, PathStrategy> sCache = new HashMap<>();
private PathStrategy mStrategy;
@Override
public boolean onCreate() {
return true;
}
@Override
public void attachInfo(Context context, ProviderInfo info) {
super.attachInfo(context, info);
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
mStrategy = getPathStrategy(context, info.authority.split(";")[0]);
}
public static Uri getUriForFile(Context context, String authority, File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
final File file = mStrategy.getFileForUri(uri);
if (projection == null) {
projection = COLUMNS;
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
@Override
public String getType(Uri uri) {
final File file = mStrategy.getFileForUri(uri);
final int lastDot = file.getName().lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file.getName().substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
final File file = mStrategy.getFileForUri(uri);
return file.delete() ? 1 : 0;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
final File file = mStrategy.getFileForUri(uri);
final int fileMode = modeToMode(mode);
return ParcelFileDescriptor.open(file, fileMode);
}
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
strat = createPathStrategy(context, authority);
sCache.put(authority, strat);
}
}
return strat;
}
private static PathStrategy createPathStrategy(Context context, String authority) {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
strat.addRoot("root_files", buildPath(DEVICE_ROOT, "."));
strat.addRoot("internal_files", buildPath(context.getFilesDir(), "."));
strat.addRoot("cache_files", buildPath(context.getCacheDir(), "."));
strat.addRoot("external_files", buildPath(Environment.getExternalStorageDirectory(), "."));
File[] externalFilesDirs = context.getExternalFilesDirs(null);
if (externalFilesDirs.length > 0) {
strat.addRoot("external_file_files", buildPath(externalFilesDirs[0], "."));
}
File[] externalCacheDirs = context.getExternalCacheDirs();
if (externalCacheDirs.length > 0) {
strat.addRoot("external_cache_files", buildPath(externalCacheDirs[0], "."));
}
File[] externalMediaDirs = context.getExternalMediaDirs();
if (externalMediaDirs.length > 0) {
strat.addRoot("external_media_files", buildPath(externalMediaDirs[0], "."));
}
return strat;
}
interface PathStrategy {
Uri getUriForFile(File file);
File getFileForUri(Uri uri);
}
static class SimplePathStrategy implements PathStrategy {
private final String mAuthority;
private final HashMap<String, File> mRoots = new HashMap<>();
SimplePathStrategy(String authority) {
mAuthority = authority;
}
void addRoot(String name, File root) {
if (TextUtils.isEmpty(name)) {
throw new IllegalArgumentException("Name must not be empty");
}
try {
root = root.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to resolve canonical path for " + root, e);
}
mRoots.put(name, root);
}
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content")
.authority(mAuthority).encodedPath(path).build();
}
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
}
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
}
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
}
return file;
}
}
private static int modeToMode(String mode) {
int modeBits;
if ("r".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_ONLY;
} else if ("w".equals(mode) || "wt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else if ("wa".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_APPEND;
} else if ("rw".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE;
} else if ("rwt".equals(mode)) {
modeBits = ParcelFileDescriptor.MODE_READ_WRITE
| ParcelFileDescriptor.MODE_CREATE
| ParcelFileDescriptor.MODE_TRUNCATE;
} else {
throw new IllegalArgumentException("Invalid mode: " + mode);
}
return modeBits;
}
private static File buildPath(File base, String... segments) {
File cur = base;
for (String segment : segments) {
if (segment != null) {
cur = new File(cur, segment);
}
}
return cur;
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
}

View File

@@ -2,6 +2,9 @@ package com.topjohnwu.magisk;
import android.content.Context; import android.content.Context;
import io.michaelrocks.paranoid.Obfuscate;
@Obfuscate
public class ProviderInstaller { public class ProviderInstaller {
public static boolean install(Context context) { public static boolean install(Context context) {

View File

@@ -1,48 +1,121 @@
package com.topjohnwu.magisk.utils; package com.topjohnwu.magisk.utils;
import android.app.Activity; import static android.content.pm.PackageInstaller.EXTRA_STATUS;
import static android.content.pm.PackageInstaller.STATUS_FAILURE_INVALID;
import static android.content.pm.PackageInstaller.STATUS_PENDING_USER_ACTION;
import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
import android.app.PendingIntent;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageInstaller.Session;
import android.content.pm.PackageInstaller.SessionParams;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.util.Log;
import com.topjohnwu.magisk.FileProvider;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class APKInstall { import io.michaelrocks.paranoid.Obfuscate;
public static Intent installIntent(Context c, File apk) { @Obfuscate
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); public final class APKInstall {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // @WorkerThread
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); public static void installapk(Context context, File apk) {
if (Build.VERSION.SDK_INT >= 24) { //noinspection InlinedApi
intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk)); var flag = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE;
} else { var action = APKInstall.class.getName();
//noinspection ResultOfMethodCallIgnored SetWorldReadable var intent = new Intent(action).setPackage(context.getPackageName());
apk.setReadable(true, false); var pending = PendingIntent.getBroadcast(context, 0, intent, flag);
intent.setData(Uri.fromFile(apk));
var installer = context.getPackageManager().getPackageInstaller();
var params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED);
}
try (Session session = installer.openSession(installer.createSession(params))) {
OutputStream out = session.openWrite(apk.getName(), 0, apk.length());
try (var in = new FileInputStream(apk); out) {
transfer(in, out);
}
session.commit(pending.getIntentSender());
} catch (IOException e) {
Log.e(APKInstall.class.getSimpleName(), "", e);
} }
return intent;
} }
public static void install(Context c, File apk) { public static void transfer(InputStream in, OutputStream out) throws IOException {
c.startActivity(installIntent(c, apk)); int size = 8192;
var buffer = new byte[size];
int read;
while ((read = in.read(buffer, 0, size)) >= 0) {
out.write(buffer, 0, read);
}
} }
public static void registerInstallReceiver(Context c, BroadcastReceiver r) { public static InstallReceiver register(Context context, String packageName, Runnable onSuccess) {
IntentFilter filter = new IntentFilter(); var receiver = new InstallReceiver(context, packageName, onSuccess);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED); var filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package"); filter.addDataScheme("package");
c.getApplicationContext().registerReceiver(r, filter); context.registerReceiver(receiver, filter);
context.registerReceiver(receiver, new IntentFilter(APKInstall.class.getName()));
return receiver;
} }
public static void installHideResult(Activity c, File apk) { public static class InstallReceiver extends BroadcastReceiver {
Intent intent = installIntent(c, apk); private final Context context;
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true); private final String packageName;
c.startActivityForResult(intent, 0); // Ignore result, use install receiver private final Runnable onSuccess;
private final CountDownLatch latch = new CountDownLatch(1);
private Intent intent = null;
private InstallReceiver(Context context, String packageName, Runnable onSuccess) {
this.context = context;
this.packageName = packageName;
this.onSuccess = onSuccess;
}
@Override
public void onReceive(Context c, Intent i) {
if (Intent.ACTION_PACKAGE_ADDED.equals(i.getAction())) {
Uri data = i.getData();
if (data == null || onSuccess == null) return;
String pkg = data.getSchemeSpecificPart();
if (pkg.equals(packageName)) {
onSuccess.run();
context.unregisterReceiver(this);
}
return;
}
int status = i.getIntExtra(EXTRA_STATUS, STATUS_FAILURE_INVALID);
switch (status) {
case STATUS_PENDING_USER_ACTION:
intent = i.getParcelableExtra(Intent.EXTRA_INTENT);
break;
case STATUS_SUCCESS:
if (onSuccess != null) onSuccess.run();
default:
context.unregisterReceiver(this);
}
latch.countDown();
}
// @WorkerThread @Nullable
public Intent waitIntent() {
try {
//noinspection ResultOfMethodCallIgnored
latch.await(5, TimeUnit.SECONDS);
} catch (Exception ignored) {
}
return intent;
}
} }
} }

View File

@@ -12,9 +12,8 @@
android:multiArch="true" android:multiArch="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"> tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
<!-- Splash -->
<activity <activity
android:name=".core.SplashActivity" android:name=".ui.MainActivity"
android:exported="true" android:exported="true"
android:theme="@style/SplashTheme"> android:theme="@style/SplashTheme">
<intent-filter> <intent-filter>
@@ -27,10 +26,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Main -->
<activity android:name=".ui.MainActivity" />
<!-- Superuser -->
<activity <activity
android:name=".ui.surequest.SuRequestActivity" android:name=".ui.surequest.SuRequestActivity"
android:directBootAware="true" android:directBootAware="true"
@@ -43,7 +38,6 @@
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Receiver -->
<receiver <receiver
android:name=".core.Receiver" android:name=".core.Receiver"
android:directBootAware="true" android:directBootAware="true"
@@ -54,15 +48,21 @@
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" /> <action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package" /> <data android:scheme="package" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<!-- DownloadService --> <service
<service android:name=".core.download.DownloadService" /> android:name=".core.download.DownloadService"
android:exported="false" />
<service
android:name=".core.JobService"
android:exported="false"
android:permission="android.permission.BIND_JOB_SERVICE" />
<!-- FileProvider -->
<provider <provider
android:name=".core.Provider" android:name=".core.Provider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -70,23 +70,18 @@
android:exported="false" android:exported="false"
android:grantUriPermissions="true" /> android:grantUriPermissions="true" />
<!-- Hardcode GMS version -->
<meta-data
android:name="com.google.android.gms.version"
android:value="12451000" />
<!-- Initialize WorkManager on-demand -->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove"
tools:ignore="ExportedContentProvider" />
<!-- We don't invalidate Room --> <!-- We don't invalidate Room -->
<service <service
android:name="androidx.room.MultiInstanceInvalidationService" android:name="androidx.room.MultiInstanceInvalidationService"
tools:node="remove" /> tools:node="remove" />
<!-- We don't need emoji compat -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="remove" />
</application> </application>
</manifest> </manifest>

View File

@@ -13,15 +13,14 @@ import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.ktx.startAnimations import com.topjohnwu.magisk.ktx.startAnimations
abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> : abstract class BaseFragment<Binding : ViewDataBinding> : Fragment(), ViewModelHolder {
Fragment(), BaseUIComponent<VM> {
val activity get() = requireActivity() as BaseUIActivity<*, *> val activity get() = getActivity() as? NavigationActivity<*>
protected lateinit var binding: Binding protected lateinit var binding: Binding
protected abstract val layoutRes: Int protected abstract val layoutRes: Int
override val viewRoot: View get() = binding.root private val navigation get() = activity?.navigation
private val navigation get() = activity.navigation open val snackbarAnchorView: View? get() = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -35,19 +34,19 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
): View? { ): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also { binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).also {
it.setVariable(BR.viewModel, viewModel) it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this it.lifecycleOwner = viewLifecycleOwner
} }
return binding.root return binding.root
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
activity.supportActionBar?.subtitle = null activity?.supportActionBar?.subtitle = null
} }
override fun onEventDispatched(event: ViewEvent) = when(event) { override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(requireContext()) is ContextExecutor -> event(requireContext())
is ActivityExecutor -> event(activity) is ActivityExecutor -> activity?.let { event(it) } ?: Unit
is FragmentExecutor -> event(this) is FragmentExecutor -> event(this)
else -> Unit else -> Unit
} }
@@ -63,7 +62,7 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.addOnRebindCallback(object : OnRebindCallback<Binding>() { binding.addOnRebindCallback(object : OnRebindCallback<Binding>() {
override fun onPreBind(binding: Binding): Boolean { override fun onPreBind(binding: Binding): Boolean {
this@BaseUIFragment.onPreBind(binding) this@BaseFragment.onPreBind(binding)
return true return true
} }
}) })
@@ -83,9 +82,3 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
} }
} }
interface ReselectionTarget {
fun onReselected()
}

View File

@@ -0,0 +1,129 @@
package com.topjohnwu.magisk.arch
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.result.contract.ActivityResultContract
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import java.util.concurrent.CountDownLatch
abstract class BaseMainActivity<Binding : ViewDataBinding> : NavigationActivity<Binding>() {
companion object {
private var doPreload = true
}
private val latch = CountDownLatch(1)
private val uninstallPkg = registerForActivityResult(UninstallPackage) { latch.countDown() }
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(Theme.selected.themeRes)
if (isRunningAsStub && doPreload) {
// Manually apply splash theme for stub
theme.applyStyle(R.style.StubSplashTheme, true)
}
super.onCreate(savedInstanceState)
if (!isRunningAsStub) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { doPreload }
}
if (doPreload) {
Shell.getShell(null) {
if (isRunningAsStub && !it.isRoot) {
showInvalidStateMessage()
return@getShell
}
preLoad()
runOnUiThread {
doPreload = false
if (isRunningAsStub) {
// Re-launch main activity without splash theme
relaunch()
} else {
showMainUI(savedInstanceState)
}
}
}
} else {
showMainUI(savedInstanceState)
}
}
abstract fun showMainUI(savedInstanceState: Bundle?)
private fun showInvalidStateMessage() {
runOnUiThread {
MagiskDialog(this).apply {
setTitle(R.string.unsupport_nonroot_stub_title)
setMessage(R.string.unsupport_nonroot_stub_msg)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { HideAPK.restore(this@BaseMainActivity) }
}
setCancelable(false)
show()
}
}
}
private fun preLoad() {
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
Config.load(prevPkg)
handleRepackage(prevPkg)
Notifications.setup(this)
JobService.schedule(this)
Shortcuts.setupDynamic(this)
// Pre-fetch network services
ServiceLocator.networkService
}
private fun handleRepackage(pkg: String?) {
if (packageName != APPLICATION_ID) {
runCatching {
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(APPLICATION_ID, 0)
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
}
} else {
if (Config.suManager.isNotEmpty())
Config.suManager = ""
pkg ?: return
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess) {
uninstallPkg.launch(pkg)
// Wait for the uninstallation to finish
latch.await()
}
}
}
object UninstallPackage : ActivityResultContract<String, Boolean>() {
@Suppress("DEPRECATION")
override fun createIntent(context: Context, input: String): Intent {
val uri = Uri.Builder().scheme("package").opaquePart(input).build()
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
return intent
}
override fun parseResult(resultCode: Int, intent: Intent?) = resultCode == RESULT_OK
}
}

View File

@@ -13,12 +13,12 @@ import androidx.navigation.NavDirections
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.databinding.ObservableHost
import com.topjohnwu.magisk.databinding.set
import com.topjohnwu.magisk.events.BackPressEvent import com.topjohnwu.magisk.events.BackPressEvent
import com.topjohnwu.magisk.events.NavigationEvent import com.topjohnwu.magisk.events.NavigationEvent
import com.topjohnwu.magisk.events.PermissionEvent import com.topjohnwu.magisk.events.PermissionEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.utils.ObservableHost
import com.topjohnwu.magisk.utils.set
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
abstract class BaseViewModel( abstract class BaseViewModel(
@@ -77,7 +77,7 @@ abstract class BaseViewModel(
PermissionEvent(permission, callback).publish() PermissionEvent(permission, callback).publish()
} }
fun withExternalRW(callback: () -> Unit) { inline fun withExternalRW(crossinline callback: () -> Unit) {
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) { withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
if (!it) { if (!it) {
SnackbarEvent(R.string.external_rw_permission_denied).publish() SnackbarEvent(R.string.external_rw_permission_denied).publish()
@@ -98,8 +98,8 @@ abstract class BaseViewModel(
_viewEvents.postValue(this) _viewEvents.postValue(this)
} }
fun NavDirections.navigate() { fun NavDirections.navigate(pop: Boolean = false) {
_viewEvents.postValue(NavigationEvent(this)) _viewEvents.postValue(NavigationEvent(this, pop))
} }
} }

View File

@@ -1,48 +0,0 @@
package com.topjohnwu.magisk.arch
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.FilterableDiffObservableList
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.OnItemBind
fun <T : ComparableRvItem<*>> diffListOf(
vararg newItems: T
) = diffListOf(newItems.toList())
fun <T : ComparableRvItem<*>> diffListOf(
newItems: List<T>
) = DiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems) }
fun <T : ComparableRvItem<*>> filterableListOf(
vararg newItems: T
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems.toList()) }
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,
layoutRes: Int,
position: Int,
item: T
) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
item.onBindingBound(binding)
}
}
inline fun <T : RvItem> itemBindingOf(
crossinline body: (ItemBinding<*>) -> Unit = {}
) = OnItemBind<T> { itemBinding, _, item ->
item.bind(itemBinding)
body(itemBinding)
}

View File

@@ -0,0 +1,35 @@
package com.topjohnwu.magisk.arch
import android.view.KeyEvent
import androidx.databinding.ViewDataBinding
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
abstract class NavigationActivity<Binding : ViewDataBinding> : UIActivity<Binding>() {
abstract val navHostId: Int
private val navHostFragment by lazy {
supportFragmentManager.findFragmentById(navHostId) as NavHostFragment
}
protected val currentFragment get() =
navHostFragment.childFragmentManager.fragments.getOrNull(0) as? BaseFragment<*>
val navigation: NavController get() = navHostFragment.navController
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
}
override fun onBackPressed() {
if (currentFragment?.onBackPressed()?.not() == true) {
super.onBackPressed()
}
}
fun NavDirections.navigate() {
navigation.navigate(this)
}
}

View File

@@ -1,17 +0,0 @@
package com.topjohnwu.magisk.arch
import android.os.Handler
import androidx.core.os.postDelayed
import com.topjohnwu.superuser.internal.UiThreadHandler
interface Queryable {
val queryDelay: Long
val queryHandler: Handler get() = UiThreadHandler.handler
fun submitQuery() {
queryHandler.postDelayed(queryDelay) { query() }
}
fun query()
}

View File

@@ -4,39 +4,25 @@ import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent
import android.view.View import android.view.View
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.res.use import androidx.core.content.res.use
import androidx.core.view.WindowCompat
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory import rikka.insets.WindowInsetsHelper
import com.topjohnwu.magisk.ui.theme.Theme import rikka.layoutinflater.view.LayoutInflaterFactory
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> : abstract class UIActivity<Binding : ViewDataBinding> : BaseActivity(), ViewModelHolder {
BaseActivity(), BaseUIComponent<VM> {
protected lateinit var binding: Binding protected lateinit var binding: Binding
protected abstract val layoutRes: Int protected abstract val layoutRes: Int
protected open val themeRes: Int = Theme.selected.themeRes
private val navHostFragment by lazy {
supportFragmentManager.findFragmentById(navHostId) as? NavHostFragment
}
private val topFragment get() = navHostFragment?.childFragmentManager?.fragments?.getOrNull(0)
protected val currentFragment get() = topFragment as? BaseUIFragment<*, *>
override val viewRoot: View get() = binding.root
open val navigation: NavController? get() = navHostFragment?.navController
open val navHostId: Int = 0
open val snackbarView get() = binding.root open val snackbarView get() = binding.root
open val snackbarAnchorView: View? get() = null
init { init {
val theme = Config.darkTheme val theme = Config.darkTheme
@@ -45,8 +31,8 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = LayoutInflaterFactory(delegate) layoutInflater.factory2 = LayoutInflaterFactory(delegate)
.addOnViewCreatedListener(WindowInsetsHelper.LISTENER)
setTheme(themeRes)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
startObserveEvents() startObserveEvents()
@@ -57,17 +43,13 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
.use { it.getDrawable(0) } .use { it.getDrawable(0) }
.also { window.setBackgroundDrawable(it) } .also { window.setBackgroundDrawable(it) }
window?.decorView?.let { WindowCompat.setDecorFitsSystemWindows(window, false)
it.systemUiVisibility = (it.systemUiVisibility
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.decorView?.post { window?.decorView?.post {
// If navigation bar is short enough (gesture navigation enabled), make it transparent // If navigation bar is short enough (gesture navigation enabled), make it transparent
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) { if ((window.decorView.rootWindowInsets?.systemWindowInsetBottom
?: 0) < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT window.navigationBarDividerColor = Color.TRANSPARENT
@@ -89,7 +71,7 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
} }
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) { fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
viewRoot.rootView.accessibilityDelegate = delegate binding.root.rootView.accessibilityDelegate = delegate
} }
override fun onResume() { override fun onResume() {
@@ -97,23 +79,9 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
viewModel.requestRefresh() viewModel.requestRefresh()
} }
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
}
override fun onEventDispatched(event: ViewEvent) = when (event) { override fun onEventDispatched(event: ViewEvent) = when (event) {
is ContextExecutor -> event(this) is ContextExecutor -> event(this)
is ActivityExecutor -> event(this) is ActivityExecutor -> event(this)
else -> Unit else -> Unit
} }
override fun onBackPressed() {
if (navigation == null || currentFragment?.onBackPressed()?.not() == true) {
super.onBackPressed()
}
}
fun NavDirections.navigate() {
navigation?.navigate(this)
}
} }

View File

@@ -18,9 +18,9 @@ interface ContextExecutor {
} }
interface ActivityExecutor { interface ActivityExecutor {
operator fun invoke(activity: BaseUIActivity<*, *>) operator fun invoke(activity: UIActivity<*>)
} }
interface FragmentExecutor { interface FragmentExecutor {
operator fun invoke(fragment: BaseUIFragment<*, *>) operator fun invoke(fragment: BaseFragment<*>)
} }

View File

@@ -1,12 +1,10 @@
package com.topjohnwu.magisk.arch package com.topjohnwu.magisk.arch
import android.view.View
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner { interface ViewModelHolder : LifecycleOwner {
val viewRoot: View val viewModel: BaseViewModel
val viewModel: VM
fun startObserveEvents() { fun startObserveEvents() {
viewModel.viewEvents.observe(this) { viewModel.viewEvents.observe(this) {

View File

@@ -6,34 +6,28 @@ import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.work.WorkManager
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.core.utils.AppShellInit import com.topjohnwu.magisk.core.utils.*
import com.topjohnwu.magisk.core.utils.BusyBoxInit
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
import com.topjohnwu.magisk.core.utils.updateConfig
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.unwrap
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.superuser.ipc.RootService
import kotlinx.coroutines.Dispatchers
import timber.log.Timber import timber.log.Timber
import java.io.File
import kotlin.system.exitProcess import kotlin.system.exitProcess
open class App() : Application() { open class App() : Application() {
constructor(o: Any) : this() { constructor(o: Any) : this() {
Info.stub = DynAPK.load(o) val data = DynAPK.Data(o)
// Add the root service name mapping
data.classToComponent[RootRegistry::class.java.name] = data.rootService.name
// Send back the actual root service class
data.rootService = RootRegistry::class.java
Info.stub = data
} }
init { init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
Shell.setDefaultBuilder(Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setInitializers(BusyBoxInit::class.java, AppShellInit::class.java)
.setTimeout(2))
Shell.EXECUTOR = IODispatcherExecutor()
// Always log full stack trace with Timber // Always log full stack trace with Timber
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())
Thread.setDefaultUncaughtExceptionHandler { _, e -> Thread.setDefaultUncaughtExceptionHandler { _, e ->
@@ -42,46 +36,57 @@ open class App() : Application() {
} }
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(context: Context) {
// Some context magic Shell.setDefaultBuilder(Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setInitializers(ShellInit::class.java)
.setTimeout(2))
Shell.EXECUTOR = DispatcherExecutor(Dispatchers.IO)
// Get the actual ContextImpl
val app: Application val app: Application
val impl: Context val base: Context
if (base is Application) { if (context is Application) {
app = base app = context
impl = base.baseContext base = context.baseContext
} else { } else {
app = this app = this
impl = base base = context
} }
val wrapped = impl.wrap() super.attachBaseContext(base)
super.attachBaseContext(wrapped) ServiceLocator.context = base
val info = base.applicationInfo refreshLocale()
val libDir = runCatching { AppApkPath = if (isRunningAsStub) {
info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String? DynAPK.current(base).path
}.getOrNull() ?: info.nativeLibraryDir } else {
Const.NATIVE_LIB_DIR = File(libDir) base.packageResourcePath
}
ServiceLocator.context = wrapped base.resources.patch()
AssetHack.init(impl) app.registerActivityLifecycleCallbacks(ActivityTracker)
app.registerActivityLifecycleCallbacks(ForegroundTracker)
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
} }
// This is required as some platforms expect ContextImpl override fun onCreate() {
override fun getBaseContext(): Context { super.onCreate()
return super.getBaseContext().unwrap() RootRegistry.bindTask = RootService.createBindTask(
intent<RootRegistry>(),
UiThreadHandler.executor,
RootRegistry.Connection
)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
resources.updateConfig(newConfig) if (resources.configuration.diff(newConfig) != 0) {
resources.setConfig(newConfig)
}
if (!isRunningAsStub) if (!isRunningAsStub)
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
object ForegroundTracker : Application.ActivityLifecycleCallbacks { object ActivityTracker : Application.ActivityLifecycleCallbacks {
@Volatile @Volatile
var foreground: Activity? = null var foreground: Activity? = null

View File

@@ -6,9 +6,9 @@ import android.util.Xml
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.refreshLocale import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.data.preference.PreferenceModel import com.topjohnwu.magisk.data.preference.PreferenceModel
import com.topjohnwu.magisk.data.repository.DBBoolSettingsNoWrite
import com.topjohnwu.magisk.data.repository.DBConfig import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.theme.Theme import com.topjohnwu.magisk.ui.theme.Theme
@@ -37,6 +37,8 @@ object Config : PreferenceModel, DBConfig {
const val SU_MULTIUSER_MODE = "multiuser_mode" const val SU_MULTIUSER_MODE = "multiuser_mode"
const val SU_MNT_NS = "mnt_ns" const val SU_MNT_NS = "mnt_ns"
const val SU_BIOMETRIC = "su_biometric" const val SU_BIOMETRIC = "su_biometric"
const val ZYGISK = "zygisk"
const val DENYLIST = "denylist"
const val SU_MANAGER = "requester" const val SU_MANAGER = "requester"
const val KEYSTORE = "keystore" const val KEYSTORE = "keystore"
@@ -59,9 +61,6 @@ object Config : PreferenceModel, DBConfig {
const val BOOT_ID = "boot_id" const val BOOT_ID = "boot_id"
const val ASKED_HOME = "asked_home" const val ASKED_HOME = "asked_home"
const val DOH = "doh" const val DOH = "doh"
// system state
const val MAGISKHIDE = "magiskhide"
} }
object Value { object Value {
@@ -111,9 +110,10 @@ object Config : PreferenceModel, DBConfig {
else else
Value.DEFAULT_CHANNEL Value.DEFAULT_CHANNEL
@JvmStatic var keepVerity = false @JvmField var keepVerity = false
@JvmStatic var keepEnc = false @JvmField var keepEnc = false
@JvmStatic var recovery = false @JvmField var patchVbmeta = false
@JvmField var recovery = false
var bootId by preference(Key.BOOT_ID, "") var bootId by preference(Key.BOOT_ID, "")
var askedHome by preference(Key.ASKED_HOME, false) var askedHome by preference(Key.ASKED_HOME, false)
@@ -133,7 +133,6 @@ object Config : PreferenceModel, DBConfig {
var suTapjack by preference(Key.SU_TAPJACK, true) var suTapjack by preference(Key.SU_TAPJACK, true)
var checkUpdate by preference(Key.CHECK_UPDATES, true) var checkUpdate by preference(Key.CHECK_UPDATES, true)
var doh by preference(Key.DOH, false) var doh by preference(Key.DOH, false)
var magiskHide by preference(Key.MAGISKHIDE, true)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false) var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "") var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
@@ -149,6 +148,8 @@ object Config : PreferenceModel, DBConfig {
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER) var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY) var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false) var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
var zygisk by dbSettings(Key.ZYGISK, false)
var denyList by DBBoolSettingsNoWrite(Key.DENYLIST, false)
var suManager by dbStrings(Key.SU_MANAGER, "", true) var suManager by dbStrings(Key.SU_MANAGER, "", true)
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true) var keyStoreRaw by dbStrings(Key.KEYSTORE, "", true)
@@ -173,12 +174,6 @@ object Config : PreferenceModel, DBConfig {
else if (it.toInt() > Value.CANARY_CHANNEL) else if (it.toInt() > Value.CANARY_CHANNEL)
putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString()) putString(Key.UPDATE_CHANNEL, Value.CANARY_CHANNEL.toString())
} }
// Write database configs
putString(Key.ROOT_ACCESS, rootMode.toString())
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
} }
} }

View File

@@ -3,58 +3,55 @@ package com.topjohnwu.magisk.core
import android.os.Build import android.os.Build
import android.os.Process import android.os.Process
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import java.io.File
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
object Const { object Const {
val CPU_ABI: String = Build.SUPPORTED_ABIS[0] val CPU_ABI: String get() = Build.SUPPORTED_ABIS[0]
val CPU_ABI_32: String = Build.SUPPORTED_32_BIT_ABIS.firstOrNull() ?: CPU_ABI
// Null if 32-bit only or 64-bit only
val CPU_ABI_32 =
if (Build.SUPPORTED_64_BIT_ABIS.isEmpty()) null
else Build.SUPPORTED_32_BIT_ABIS.firstOrNull()
// Paths // Paths
lateinit var MAGISKTMP: String lateinit var MAGISKTMP: String
lateinit var NATIVE_LIB_DIR: File
val MAGISK_PATH get() = "$MAGISKTMP/modules" val MAGISK_PATH get() = "$MAGISKTMP/modules"
const val TMPDIR = "/dev/tmp" const val TMPDIR = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log" const val MAGISK_LOG = "/cache/magisk.log"
// Versions
const val SNET_EXT_VER = 17
const val SNET_REVISION = "23.0"
const val BOOTCTL_REVISION = "22.0"
// Misc // Misc
val USER_ID = Process.myUid() / 100000 val USER_ID = Process.myUid() / 100000
val APP_IS_CANARY get() = Version.isCanary(BuildConfig.VERSION_CODE)
object Version { object Version {
const val MIN_VERSION = "v20.4" const val MIN_VERSION = "v21.0"
const val MIN_VERCODE = 20400 const val MIN_VERCODE = 21000
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary() fun atLeast_21_2() = Info.env.versionCode >= 21200 || isCanary()
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary() fun atLeast_24_0() = Info.env.versionCode >= 24000 || isCanary()
fun isCanary() = Info.env.magiskVersionCode % 100 != 0 fun isCanary() = isCanary(Info.env.versionCode)
fun isCanary(ver: Int) = ver > 0 && ver % 100 != 0
} }
object ID { object ID {
// notifications const val JOB_SERVICE_ID = 7
const val APK_UPDATE_NOTIFICATION_ID = 5
const val UPDATE_NOTIFICATION_CHANNEL = "update" const val UPDATE_NOTIFICATION_CHANNEL = "update"
const val PROGRESS_NOTIFICATION_CHANNEL = "progress" const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
} }
object Url { object Url {
const val PATREON_URL = "https://www.patreon.com/topjohnwu" const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk" const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
val CHANGELOG_URL = if (BuildConfig.VERSION_CODE % 100 != 0) Info.remote.magisk.note val CHANGELOG_URL = if (APP_IS_CANARY) Info.remote.magisk.note
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md" else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/" const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
const val GITHUB_API_URL = "https://api.github.com/" const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/" const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk-files/"
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/" const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
} }
object Key { object Key {
@@ -74,7 +71,6 @@ object Const {
object Nav { object Nav {
const val HOME = "home" const val HOME = "home"
const val SETTINGS = "settings" const val SETTINGS = "settings"
const val HIDE = "hide"
const val MODULES = "modules" const val MODULES = "modules"
const val SUPERUSER = "superuser" const val SUPERUSER = "superuser"
} }

View File

@@ -2,11 +2,7 @@
package com.topjohnwu.magisk.core package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.app.job.JobWorkItem
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
@@ -15,33 +11,38 @@ import android.content.res.AssetManager
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.utils.refreshLocale import com.topjohnwu.magisk.core.utils.syncLocale
import com.topjohnwu.magisk.core.utils.updateConfig import com.topjohnwu.magisk.di.AppContext
fun AssetManager.addAssetPath(path: String) { lateinit var AppApkPath: String
DynAPK.addAssetPath(this, path)
fun AssetManager.addAssetPath(path: String) = DynAPK.addAssetPath(this, path)
fun Context.wrap(): Context = if (this is PatchedContext) this else PatchedContext(this)
private class PatchedContext(base: Context) : ContextWrapper(base) {
init { base.resources.patch() }
override fun getClassLoader() = javaClass.classLoader!!
override fun createConfigurationContext(config: Configuration) =
super.createConfigurationContext(config).wrap()
} }
fun Context.wrap(inject: Boolean = false): Context = fun Resources.patch(): Resources {
if (inject) ReInjectedContext(this) else InjectedContext(this) syncLocale()
if (isRunningAsStub)
assets.addAssetPath(AppApkPath)
return this
}
fun Context.wrapJob(): Context = object : InjectedContext(this) { fun createNewResources(): Resources {
val asset = AssetManager::class.java.newInstance()
override fun getApplicationContext() = this asset.addAssetPath(AppApkPath)
val config = Configuration(AppContext.resources.configuration)
@SuppressLint("NewApi") val metrics = DisplayMetrics()
override fun getSystemService(name: String): Any? { metrics.setTo(AppContext.resources.displayMetrics)
return super.getSystemService(name).let { return Resources(asset, metrics, config)
when {
!isRunningAsStub -> it
name == JOB_SCHEDULER_SERVICE -> JobSchedulerWrapper(it as JobScheduler)
else -> it
}
}
}
} }
fun Class<*>.cmp(pkg: String) = fun Class<*>.cmp(pkg: String) =
@@ -53,74 +54,6 @@ inline fun <reified T> Activity.redirect() = Intent(intent)
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName)) inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
private open class InjectedContext(base: Context) : ContextWrapper(base) {
open val res: Resources get() = AssetHack.resource
override fun getAssets(): AssetManager = res.assets
override fun getResources() = res
override fun getClassLoader() = javaClass.classLoader!!
override fun createConfigurationContext(config: Configuration): Context {
return super.createConfigurationContext(config).wrap(true)
}
}
private class ReInjectedContext(base: Context) : InjectedContext(base) {
override val res by lazy { base.resources.patch() }
private fun Resources.patch(): Resources {
updateConfig()
if (isRunningAsStub)
assets.addAssetPath(AssetHack.apk)
return this
}
}
object AssetHack {
lateinit var resource: Resources
lateinit var apk: String
fun init(context: Context) {
resource = context.resources
refreshLocale()
if (isRunningAsStub) {
apk = DynAPK.current(context).path
resource.assets.addAssetPath(apk)
} else {
apk = context.packageResourcePath
}
}
fun newResource(): Resources {
val asset = AssetManager::class.java.newInstance()
asset.addAssetPath(apk)
val config = Configuration(resource.configuration)
val metrics = DisplayMetrics()
metrics.setTo(resource.displayMetrics)
return Resources(asset, metrics, config)
}
}
@RequiresApi(28)
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
override fun schedule(job: JobInfo) = base.schedule(job.patch())
override fun enqueue(job: JobInfo, work: JobWorkItem) = base.enqueue(job.patch(), work)
override fun cancel(jobId: Int) = base.cancel(jobId)
override fun cancelAll() = base.cancelAll()
override fun getAllPendingJobs(): List<JobInfo> = base.allPendingJobs
override fun getPendingJob(jobId: Int) = base.getPendingJob(jobId)
private fun JobInfo.patch(): JobInfo {
// Swap out the service of JobInfo
val component = service.run {
ComponentName(packageName,
Info.stub?.classToComponent?.get(className) ?: className)
}
javaClass.getDeclaredField("service").apply {
isAccessible = true
}.set(this, component)
return this
}
}
// Keep a reference to these resources to prevent it from // Keep a reference to these resources to prevent it from
// being removed when running "remove unused resources" // being removed when running "remove unused resources"
val shouldKeepResources = listOf( val shouldKeepResources = listOf(
@@ -128,9 +61,11 @@ val shouldKeepResources = listOf(
R.string.release_notes, R.string.release_notes,
R.string.invalid_update_channel, R.string.invalid_update_channel,
R.string.update_available, R.string.update_available,
R.string.safetynet_api_error,
R.drawable.ic_device, R.drawable.ic_device,
R.drawable.ic_hide_select_md2,
R.drawable.ic_more, R.drawable.ic_more,
R.drawable.ic_magisk_delete R.drawable.ic_magisk_delete,
R.drawable.ic_refresh_data_md2,
R.drawable.ic_order_date,
R.drawable.ic_order_name,
R.array.allow_timeout,
) )

View File

@@ -11,8 +11,6 @@ import com.topjohnwu.magisk.ktx.getProperty
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils.fastCmd import com.topjohnwu.superuser.ShellUtils.fastCmd
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.File
import java.io.IOException
val isRunningAsStub get() = Info.stub != null val isRunningAsStub get() = Info.stub != null
@@ -31,16 +29,20 @@ object Info {
// Device state // Device state
@JvmStatic val env by lazy { loadState() } @JvmStatic val env by lazy { loadState() }
@JvmField var isSAR = false @JvmField var isSAR = false
@JvmField var isAB = false var isAB = false
@JvmField val isVirtualAB = getProperty("ro.virtual_ab.enabled", "false") == "true" @JvmField val isZygiskEnabled = System.getenv("ZYGISK_ENABLED") == "1"
@JvmStatic val isFDE get() = crypto == "block" @JvmStatic val isFDE get() = crypto == "block"
@JvmField var ramdisk = false @JvmField var ramdisk = false
@JvmField var hasGMS = true @JvmField var vbmeta = false
@JvmField val isPixel = Build.BRAND == "google"
@JvmField val isEmulator = getProperty("ro.kernel.qemu", "0") == "1"
var crypto = "" var crypto = ""
var noDataExec = false var noDataExec = false
@JvmField var hasGMS = true
val isSamsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
@JvmField val isEmulator =
getProperty("ro.kernel.qemu", "0") == "1" ||
getProperty("ro.boot.qemu", "0") == "1"
val isConnected by lazy { val isConnected by lazy {
ObservableBoolean(false).also { field -> ObservableBoolean(false).also { field ->
NetworkObserver.observe(AppContext) { NetworkObserver.observe(AppContext) {
@@ -49,41 +51,20 @@ object Info {
} }
} }
val isNewReboot by lazy {
try {
val id = File("/proc/sys/kernel/random/boot_id").readText()
if (id != Config.bootId) {
Config.bootId = id
true
} else {
false
}
} catch (e: IOException) {
false
}
}
private fun loadState() = Env( private fun loadState() = Env(
fastCmd("magisk -v").split(":".toRegex())[0], fastCmd("magisk -v").split(":".toRegex())[0],
runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1), runCatching { fastCmd("magisk -V").toInt() }.getOrDefault(-1)
Shell.su("magiskhide status").exec().isSuccess
) )
class Env( class Env(
val magiskVersionString: String = "", val versionString: String = "",
code: Int = -1, code: Int = -1
hide: Boolean = false
) { ) {
val magiskHide get() = Config.magiskHide val versionCode = when {
val magiskVersionCode = when {
code < Const.Version.MIN_VERCODE -> -1 code < Const.Version.MIN_VERCODE -> -1
else -> if (Shell.rootAccess()) code else -1 else -> if (Shell.rootAccess()) code else -1
} }
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
val isActive = magiskVersionCode >= 0 val isActive = versionCode >= 0
init {
Config.magiskHide = hide
}
} }
} }

View File

@@ -0,0 +1,57 @@
package com.topjohnwu.magisk.core
import android.app.job.JobInfo
import android.app.job.JobParameters
import android.app.job.JobScheduler
import android.content.Context
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.base.BaseJobService
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.util.concurrent.TimeUnit
class JobService : BaseJobService() {
private val job = Job()
private val svc get() = ServiceLocator.networkService
override fun onStartJob(params: JobParameters): Boolean {
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
coroutineScope.launch {
svc.fetchUpdate()?.run {
Info.remote = this
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
Notifications.managerUpdate(this@JobService)
}
jobFinished(params, false)
}
return false
}
override fun onStopJob(params: JobParameters): Boolean {
job.cancel()
return false
}
companion object {
fun schedule(context: Context) {
val scheduler = context.getSystemService<JobScheduler>() ?: return
if (Config.checkUpdate) {
val cmp = JobService::class.java.cmp(context.packageName)
val info = JobInfo.Builder(Const.ID.JOB_SERVICE_ID, cmp)
.setPeriodic(TimeUnit.HOURS.toMillis(12))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setRequiresDeviceIdle(true)
.build()
scheduler.schedule(info)
} else {
scheduler.cancel(Const.ID.JOB_SERVICE_ID)
}
}
}
}

View File

@@ -1,23 +1,25 @@
package com.topjohnwu.magisk.core package com.topjohnwu.magisk.core
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.pm.ProviderInfo import android.content.pm.ProviderInfo
import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler import com.topjohnwu.magisk.core.su.SuCallbackHandler
import java.io.File import java.io.File
open class Provider : FileProvider() { class Provider : ContentProvider() {
override fun attachInfo(context: Context, info: ProviderInfo?) { override fun attachInfo(context: Context, info: ProviderInfo) {
super.attachInfo(context.wrap(), info) super.attachInfo(context.wrap(), info)
} }
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
SuCallbackHandler(context!!, method, extras) SuCallbackHandler.run(context!!, method, extras)
return Bundle.EMPTY return Bundle.EMPTY
} }
@@ -36,4 +38,11 @@ open class Provider : FileProvider() {
fun PREFS_URI(pkg: String) = fun PREFS_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build() Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
} }
override fun onCreate() = true
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?) = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?) = 0
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? = null
} }

View File

@@ -40,7 +40,9 @@ open class Receiver : BaseReceiver() {
} }
Intent.ACTION_UID_REMOVED -> { Intent.ACTION_UID_REMOVED -> {
getUid(intent)?.let { rmPolicy(it) } getUid(intent)?.let { rmPolicy(it) }
getPkg(intent)?.let { Shell.su("magiskhide rm $it").submit() } }
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
getPkg(intent)?.let { Shell.su("magisk --denylist rm $it").submit() }
} }
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context) Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setupDynamic(context)
} }

View File

@@ -1,91 +0,0 @@
package com.topjohnwu.magisk.core
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import java.util.concurrent.CountDownLatch
open class SplashActivity : BaseActivity() {
private val latch = CountDownLatch(1)
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.SplashTheme)
super.onCreate(savedInstanceState)
// Pre-initialize root shell
Shell.getShell(null) { initAndStart() }
}
private fun handleRepackage(pkg: String?) {
if (packageName != APPLICATION_ID) {
runCatching {
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(APPLICATION_ID, 0)
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
}
} else {
if (Config.suManager.isNotEmpty())
Config.suManager = ""
pkg ?: return
if (!Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec().isSuccess)
uninstallApp(pkg)
}
}
private fun initAndStart() {
if (isRunningAsStub && !Shell.rootAccess()) {
runOnUiThread {
MagiskDialog(this)
.applyTitle(R.string.unsupport_nonroot_stub_title)
.applyMessage(R.string.unsupport_nonroot_stub_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.install
onClick { HideAPK.restore(this@SplashActivity) }
}
.cancellable(false)
.reveal()
}
return
}
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
Config.load(prevPkg)
handleRepackage(prevPkg)
Notifications.setup(this)
UpdateCheckService.schedule(this)
Shortcuts.setupDynamic(this)
// Pre-fetch network services
ServiceLocator.networkService
DONE = true
startActivity(redirect<MainActivity>())
finish()
}
@Suppress("DEPRECATION")
private fun uninstallApp(pkg: String) {
val uri = Uri.Builder().scheme("package").opaquePart(pkg).build()
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, uri)
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
startActivityForResult(intent) { _, _ ->
latch.countDown()
}
latch.await()
}
companion object {
var DONE = false
}
}

View File

@@ -1,44 +0,0 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.content.Context
import androidx.work.*
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.Notifications
import java.util.concurrent.TimeUnit
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
: CoroutineWorker(context, workerParams) {
private val svc get() = ServiceLocator.networkService
override suspend fun doWork(): Result {
return svc.fetchUpdate()?.run {
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
Notifications.managerUpdate(applicationContext)
Result.success()
} ?: Result.failure()
}
companion object {
@SuppressLint("NewApi")
fun schedule(context: Context) {
if (Config.checkUpdate) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresDeviceIdle(true)
.build()
val request = PeriodicWorkRequestBuilder<UpdateCheckService>(12, TimeUnit.HOURS)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID,
ExistingPeriodicWorkPolicy.REPLACE, request)
} else {
WorkManager.getInstance(context)
.cancelUniqueWork(Const.ID.CHECK_MAGISK_UPDATE_WORKER_ID)
}
}
}
}

View File

@@ -1,38 +1,32 @@
package com.topjohnwu.magisk.core.base package com.topjohnwu.magisk.core.base
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts.GetContent
import androidx.annotation.CallSuper import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.collection.SparseArrayCompat import com.topjohnwu.magisk.core.isRunningAsStub
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.utils.currentLocale import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.core.wrap import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ktx.reflectField import com.topjohnwu.magisk.ktx.reflectField
import com.topjohnwu.magisk.ktx.set
import com.topjohnwu.magisk.utils.Utils
import kotlin.random.Random
typealias ActivityResultCallback = BaseActivity.(Int, Intent?) -> Unit
abstract class BaseActivity : AppCompatActivity() { abstract class BaseActivity : AppCompatActivity() {
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() } private var permissionCallback: ((Boolean) -> Unit)? = null
private val newRequestCode: Int get() { private val requestPermission = registerForActivityResult(RequestPermission()) {
var requestCode: Int permissionCallback?.invoke(it)
do { permissionCallback = null
requestCode = Random.nextInt(0, 1 shl 15) }
} while (resultCallbacks.containsKey(requestCode))
return requestCode private var contentCallback: ((Uri) -> Unit)? = null
private val getContent = registerForActivityResult(GetContent()) {
if (it != null) contentCallback?.invoke(it)
contentCallback = null
} }
override fun applyOverrideConfiguration(config: Configuration?) { override fun applyOverrideConfiguration(config: Configuration?) {
@@ -42,79 +36,33 @@ abstract class BaseActivity : AppCompatActivity() {
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap(true)) super.attachBaseContext(base.wrap())
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// Overwrite private members to avoid nasty "false" stack traces being logged if (isRunningAsStub) {
val delegate = delegate // Overwrite private members to avoid nasty "false" stack traces being logged
val clz = delegate.javaClass val delegate = delegate
clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true) val clz = delegate.javaClass
clz.reflectField("mActivityHandlesUiMode").set(delegate, false) clz.reflectField("mActivityHandlesUiModeChecked").set(delegate, true)
clz.reflectField("mActivityHandlesUiMode").set(delegate, false)
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) { fun withPermission(permission: String, callback: (Boolean) -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build()
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) { if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
// We do not need external rw on 30+ // We do not need external rw on 30+
request.onSuccess() callback(true)
return return
} }
permissionCallback = callback
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { requestPermission.launch(permission)
request.onSuccess()
} else {
val requestCode = newRequestCode
resultCallbacks[requestCode] = { result, _ ->
if (result > 0)
request.onSuccess()
else
request.onFailure()
}
ActivityCompat.requestPermissions(this, arrayOf(permission), requestCode)
}
} }
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) { fun getContent(type: String, callback: (Uri) -> Unit) {
withPermission(WRITE_EXTERNAL_STORAGE, builder = builder) contentCallback = callback
} getContent.launch(type)
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
var success = true
for (res in grantResults) {
if (res != PackageManager.PERMISSION_GRANTED) {
success = false
break
}
}
resultCallbacks[requestCode]?.also {
resultCallbacks.remove(requestCode)
it(this, if (success) 1 else -1, null)
}
}
@CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.also { callback ->
resultCallbacks.remove(requestCode)
callback(this, resultCode, data)
}
}
fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
val requestCode = newRequestCode
resultCallbacks[requestCode] = callback
try {
startActivityForResult(intent, requestCode)
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
} }
override fun recreate() { override fun recreate() {
@@ -122,4 +70,8 @@ abstract class BaseActivity : AppCompatActivity() {
finish() finish()
} }
fun relaunch() {
startActivity(Intent(intent).setFlags(0))
finish()
}
} }

View File

@@ -0,0 +1,11 @@
package com.topjohnwu.magisk.core.base
import android.app.job.JobService
import android.content.Context
import com.topjohnwu.magisk.core.wrap
abstract class BaseJobService : JobService() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
}

View File

@@ -1,59 +0,0 @@
package com.topjohnwu.magisk.core.base
import android.content.Context
import android.net.Network
import android.net.Uri
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.work.Data
import androidx.work.ListenableWorker
import com.google.common.util.concurrent.ListenableFuture
import java.util.*
abstract class BaseWorkerWrapper {
private lateinit var worker: ListenableWorker
val applicationContext: Context
get() = worker.applicationContext
val id: UUID
get() = worker.id
val inputData: Data
get() = worker.inputData
val tags: Set<String>
get() = worker.tags
val triggeredContentUris: List<Uri>
@RequiresApi(24)
get() = worker.triggeredContentUris
val triggeredContentAuthorities: List<String>
@RequiresApi(24)
get() = worker.triggeredContentAuthorities
val network: Network?
@RequiresApi(28)
get() = worker.network
val runAttemptCount: Int
get() = worker.runAttemptCount
val isStopped: Boolean
get() = worker.isStopped
abstract fun doWork(): ListenableWorker.Result
fun onStopped() {}
fun attachWorker(w: ListenableWorker) {
worker = w
}
@MainThread
fun startWork(): ListenableFuture<ListenableWorker.Result> {
return worker.startWork()
}
}

View File

@@ -1,40 +0,0 @@
package com.topjohnwu.magisk.core.base
typealias SimpleCallback = () -> Unit
typealias PermissionRationaleCallback = (List<String>) -> Unit
class PermissionRequestBuilder {
private var onSuccessCallback: SimpleCallback = {}
private var onFailureCallback: SimpleCallback = {}
private var onShowRationaleCallback: PermissionRationaleCallback = {}
fun onSuccess(callback: SimpleCallback) {
onSuccessCallback = callback
}
fun onFailure(callback: SimpleCallback) {
onFailureCallback = callback
}
fun onShowRationale(callback: PermissionRationaleCallback) {
onShowRationaleCallback = callback
}
fun build(): PermissionRequest {
return PermissionRequest(onSuccessCallback, onFailureCallback, onShowRationaleCallback)
}
}
class PermissionRequest(
private val onSuccessCallback: SimpleCallback,
private val onFailureCallback: SimpleCallback,
private val onShowRationaleCallback: PermissionRationaleCallback
) {
fun onSuccess() = onSuccessCallback()
fun onFailure() = onFailureCallback()
fun onShowRationale(permissions: List<String>) = onShowRationaleCallback(permissions)
}

View File

@@ -1,192 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.app.Notification
import android.content.Intent
import android.os.IBinder
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ForegroundTracker
import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber
import java.io.IOException
import java.io.InputStream
import java.util.*
import kotlin.collections.HashMap
import kotlin.random.Random.Default.nextInt
abstract class BaseDownloader : BaseService() {
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
private val coroutineScope = CoroutineScope(Dispatchers.IO)
val service get() = ServiceLocator.networkService
// -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(ACTION_KEY)?.let { subject ->
update(subject.notifyID())
coroutineScope.launch {
try {
subject.startDownload()
} catch (e: IOException) {
Timber.e(e)
notifyFail(subject)
}
}
}
return START_REDELIVER_INTENT
}
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { cancel(it.key) }
notifications.clear()
}
override fun onDestroy() {
super.onDestroy()
coroutineScope.cancel()
}
// -- Download logic
private suspend fun Subject.startDownload() {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
is Subject.Manager -> handleAPK(this, stream)
}
val newId = notifyFinish(this)
if (ForegroundTracker.hasForeground)
onFinish(this, newId)
if (!hasNotifications)
stopSelf()
}
private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
val max = contentLength()
val total = max.toFloat() / 1048576
val id = subject.notifyID()
update(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
update(id) { notification ->
if (max > 0) {
broadcast(progress / total, subject)
notification
.setProgress(max.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, total))
} else {
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
// --- Notification managements
fun Subject.notifyID() = hashCode()
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
private fun notifyFinish(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(1f, subject)
it.setIntent(subject)
.setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
}
private fun create() = Notifications.progress(this, "")
fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
notify(id, notification.build())
}
private fun lastNotify(
id: Int,
editor: (Notification.Builder) -> Notification.Builder? = { null }
) : Int {
val notification = remove(id)?.run(editor) ?: return -1
val newId: Int = nextInt()
notify(newId, notification.build())
return newId
}
protected fun remove(id: Int) = notifications.remove(id)
?.also { updateForeground(); cancel(id) }
?: { cancel(id); null }()
private fun notify(id: Int, notification: Notification) {
Notifications.mgr.notify(id, notification)
}
private fun cancel(id: Int) {
Notifications.mgr.cancel(id)
}
private fun updateForeground() {
if (hasNotifications) {
val (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
// --- Implement custom logic
protected abstract suspend fun onFinish(subject: Subject, id: Int)
protected abstract fun Notification.Builder.setIntent(subject: Subject): Notification.Builder
// ---
companion object {
const val ACTION_KEY = "download_action"
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
progressBroadcast.value = null
progressBroadcast.observe(owner) {
val (progress, subject) = it ?: return@observe
callback(progress, subject)
}
}
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
}
}

View File

@@ -1,82 +1,193 @@
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import androidx.core.net.toFile import android.os.IBinder
import com.topjohnwu.magisk.arch.BaseUIActivity import androidx.lifecycle.LifecycleOwner
import com.topjohnwu.magisk.core.ForegroundTracker import androidx.lifecycle.MutableLiveData
import com.topjohnwu.magisk.core.download.Action.Flash import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.Subject.Manager import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.download.Subject.Module import com.topjohnwu.magisk.core.base.BaseService
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.ui.flash.FlashFragment import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.magisk.ktx.synchronized
import kotlin.random.Random.Default.nextInt import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Notifications.mgr
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import okhttp3.ResponseBody
import timber.log.Timber
import java.io.InputStream
open class DownloadService : BaseDownloader() { class DownloadService : BaseService() {
private val context get() = this private val hasNotifications get() = notifications.isNotEmpty()
private val notifications = HashMap<Int, Notification.Builder>().synchronized()
private val job = Job()
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) { val service get() = ServiceLocator.networkService
is Module -> subject.onFinish(id)
is Manager -> subject.onFinish(id) // -- Service overrides
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
intent.getParcelableExtra<Subject>(SUBJECT_KEY)?.let { doDownload(it) }
return START_NOT_STICKY
} }
private fun Module.onFinish(id: Int) = when (action) { override fun onTaskRemoved(rootIntent: Intent?) {
Flash -> { super.onTaskRemoved(rootIntent)
UiThreadHandler.run { notifications.forEach { mgr.cancel(it.key) }
(ForegroundTracker.foreground as? BaseUIActivity<*, *>) notifications.clear()
?.navigation?.navigate(FlashFragment.install(file, id)) }
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
// -- Download logic
private fun doDownload(subject: Subject) {
update(subject.notifyId)
val coroutineScope = CoroutineScope(job + Dispatchers.IO)
coroutineScope.launch {
try {
val stream = service.fetchFile(subject.url).toProgressStream(subject)
when (subject) {
is Subject.Manager -> handleAPK(subject, stream)
is Subject.Module -> stream.toModule(subject.file, assets.open("module_installer.sh"))
}
val activity = ActivityTracker.foreground
if (activity != null && subject.autoStart) {
remove(subject.notifyId)
subject.pendingIntent(activity).send()
} else {
notifyFinish(subject)
}
if (!hasNotifications)
stopSelf()
} catch (e: Exception) {
Timber.e(e)
notifyFail(subject)
} }
} }
else -> Unit
} }
private fun Manager.onFinish(id: Int) { private fun ResponseBody.toProgressStream(subject: Subject): InputStream {
remove(id) val max = contentLength()
APKInstall.install(context, file.toFile()) val total = max.toFloat() / 1048576
val id = subject.notifyId
update(id) { it.setContentTitle(subject.title) }
return ProgressInputStream(byteStream()) {
val progress = it.toFloat() / 1048576
update(id) { notification ->
if (max > 0) {
broadcast(progress / total, subject)
notification
.setProgress(max.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, total))
} else {
broadcast(-1f, subject)
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
} }
// --- Customize finish notification // --- Notification management
override fun Notification.Builder.setIntent(subject: Subject) private fun notifyFail(subject: Subject) = finalNotify(subject.notifyId) {
= when (subject) { broadcast(-2f, subject)
is Module -> setIntent(subject) it.setContentText(getString(R.string.download_file_error))
is Manager -> setIntent(subject) .setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
} }
private fun Notification.Builder.setIntent(subject: Module) private fun notifyFinish(subject: Subject) = finalNotify(subject.notifyId) {
= when (subject.action) { broadcast(1f, subject)
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file)) it.setContentIntent(subject.pendingIntent(this))
else -> setContentIntent(Intent()) .setContentTitle(subject.title)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
} }
private fun Notification.Builder.setIntent(subject: Manager) private fun finalNotify(id: Int, editor: (Notification.Builder) -> Unit): Int {
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile())) val notification = remove(id)?.also(editor) ?: return -1
val newId = Notifications.nextId()
mgr.notify(newId, notification.build())
return newId
}
private fun Notification.Builder.setContentIntent(intent: Intent) = private fun create() = Notifications.progress(this, "")
setContentIntent(
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
)
// --- fun update(id: Int, editor: (Notification.Builder) -> Unit = {}) {
val wasEmpty = !hasNotifications
val notification = notifications.getOrPut(id, ::create).also(editor)
if (wasEmpty)
updateForeground()
else
mgr.notify(id, notification.build())
}
private fun remove(id: Int): Notification.Builder? {
val n = notifications.remove(id)?.also { updateForeground() }
mgr.cancel(id)
return n
}
private fun updateForeground() {
if (hasNotifications) {
val (id, notification) = notifications.entries.first()
startForeground(id, notification.build())
} else {
stopForeground(false)
}
}
companion object { companion object {
private const val SUBJECT_KEY = "download_subject"
private const val REQUEST_CODE = 1
private val progressBroadcast = MutableLiveData<Pair<Float, Subject>?>()
fun observeProgress(owner: LifecycleOwner, callback: (Float, Subject) -> Unit) {
progressBroadcast.value = null
progressBroadcast.observe(owner) {
val (progress, subject) = it ?: return@observe
callback(progress, subject)
}
}
private fun broadcast(progress: Float, subject: Subject) {
progressBroadcast.postValue(progress to subject)
}
private fun intent(context: Context, subject: Subject) = private fun intent(context: Context, subject: Subject) =
context.intent<DownloadService>().putExtra(ACTION_KEY, subject) context.intent<DownloadService>().putExtra(SUBJECT_KEY, subject)
fun pendingIntent(context: Context, subject: Subject): PendingIntent { @SuppressLint("InlinedApi")
fun getPendingIntent(context: Context, subject: Subject): PendingIntent {
val flag = FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT or FLAG_ONE_SHOT
val intent = intent(context, subject)
return if (Build.VERSION.SDK_INT >= 26) { return if (Build.VERSION.SDK_INT >= 26) {
PendingIntent.getForegroundService(context, nextInt(), getForegroundService(context, REQUEST_CODE, intent, flag)
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
} else { } else {
PendingIntent.getService(context, nextInt(), getService(context, REQUEST_CODE, intent, flag)
intent(context, subject), PendingIntent.FLAG_UPDATE_CURRENT)
} }
} }

View File

@@ -1,36 +1,24 @@
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.content.Context import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Intent
import androidx.core.content.getSystemService
import androidx.core.net.toFile import androidx.core.net.toFile
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.ActivityTracker
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.HideAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.relaunchApp import com.topjohnwu.magisk.ktx.copyAndClose
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
private fun Context.patch(apk: File) { private class TeeOutputStream(
val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
}
private fun BaseDownloader.notifyHide(id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
}
private class DupOutputStream(
private val o1: OutputStream, private val o1: OutputStream,
private val o2: OutputStream private val o2: OutputStream
) : OutputStream() { ) : OutputStream() {
@@ -48,26 +36,40 @@ private class DupOutputStream(
} }
} }
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager, stream: InputStream) { suspend fun DownloadService.handleAPK(subject: Subject.Manager, stream: InputStream) {
fun write(output: OutputStream) { fun write(output: OutputStream) {
val ext = subject.externalFile.outputStream() val external = subject.externalFile.outputStream()
val o = DupOutputStream(ext, output) stream.copyAndClose(TeeOutputStream(external, output))
withStreams(stream, o) { src, out -> src.copyTo(out) }
} }
if (isRunningAsStub) { if (isRunningAsStub) {
val apk = subject.file.toFile() val apk = subject.file.toFile()
val id = subject.notifyID() val id = subject.notifyId
write(DynAPK.update(this).outputStream()) write(DynAPK.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) { if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub // Also upgrade stub
notifyHide(id) update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
service.fetchFile(subject.stub.link).byteStream().writeTo(apk) service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
patch(apk) val patched = File(apk.parent, "patched.apk")
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
} else { } else {
// Simply relaunch the app val intent = packageManager.getLaunchIntentForPackage(packageName)
intent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//noinspection InlinedApi
val flag = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
val pending = PendingIntent.getActivity(this, id, intent, flag)
if (ActivityTracker.hasForeground) {
val alarm = getSystemService<AlarmManager>()
alarm!!.set(AlarmManager.RTC, System.currentTimeMillis() + 1000, pending)
}
stopSelf() stopSelf()
relaunchApp(this) Runtime.getRuntime().exit(0)
} }
} else { } else {
write(subject.file.outputStream()) write(subject.file.outputStream())

View File

@@ -25,13 +25,8 @@ fun InputStream.toModule(file: Uri, installer: InputStream) {
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script")) zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray(charset("UTF-8"))) zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
var off = -1
zin.forEach { entry -> zin.forEach { entry ->
if (off < 0) { val path = entry.name
off = entry.name.indexOf('/') + 1
}
val path = entry.name.substring(off)
if (path.isNotEmpty() && !path.startsWith("META-INF")) { if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path)) zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) { if (!entry.isDirectory) {

View File

@@ -1,7 +1,12 @@
package com.topjohnwu.magisk.core.download package com.topjohnwu.magisk.core.download
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.core.net.toFile
import androidx.core.net.toUri import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson import com.topjohnwu.magisk.core.model.MagiskJson
@@ -10,38 +15,54 @@ import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.ktx.cachedFile import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.view.Notifications
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri() private fun cachedFile(name: String) = AppContext.cachedFile(name).apply { delete() }.toUri()
enum class Action {
Flash,
Download
}
sealed class Subject : Parcelable { sealed class Subject : Parcelable {
abstract val url: String abstract val url: String
abstract val file: Uri abstract val file: Uri
abstract val action: Action
abstract val title: String abstract val title: String
abstract val notifyId: Int
open val autoStart: Boolean get() = true
abstract fun pendingIntent(context: Context): PendingIntent
@Parcelize @Parcelize
class Module( class Module(
val module: OnlineModule, val module: OnlineModule,
override val action: Action val action: Action,
override val notifyId: Int = Notifications.nextId()
) : Subject() { ) : Subject() {
override val url: String get() = module.zip_url override val url: String get() = module.zipUrl
override val title: String get() = module.downloadFilename override val title: String get() = module.downloadFilename
override val autoStart: Boolean get() = action == Action.Flash
@IgnoredOnParcel @IgnoredOnParcel
override val file by lazy { override val file by lazy {
MediaStoreUtils.getFile(title).uri MediaStoreUtils.getFile(title).uri
} }
override fun pendingIntent(context: Context) =
FlashFragment.installIntent(context, file)
} }
@Parcelize @Parcelize
class Manager( class Manager(
private val json: MagiskJson = Info.remote.magisk, private val json: MagiskJson = Info.remote.magisk,
val stub: StubJson = Info.remote.stub val stub: StubJson = Info.remote.stub,
override val notifyId: Int = Notifications.nextId()
) : Subject() { ) : Subject() {
override val action get() = Action.Download
override val title: String get() = "Magisk-${json.version}(${json.versionCode})" override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
override val url: String get() = json.link override val url: String get() = json.link
@@ -51,13 +72,18 @@ sealed class Subject : Parcelable {
} }
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
override fun pendingIntent(context: Context): PendingIntent {
val receiver = APKInstall.register(context, null, null)
APKInstall.installapk(context, file.toFile())
val intent = receiver.waitIntent() ?: Intent()
return intent.toPending(context)
}
}
@SuppressLint("InlinedApi")
protected fun Intent.toPending(context: Context): PendingIntent {
return PendingIntent.getActivity(context, notifyId, this,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_ONE_SHOT)
} }
} }
sealed class Action : Parcelable {
@Parcelize
object Flash : Action()
@Parcelize
object Download : Action()
}

View File

@@ -1,9 +1,7 @@
package com.topjohnwu.magisk.core.magiskdb package com.topjohnwu.magisk.core.magiskdb
import android.content.pm.PackageManager
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.toMap
import com.topjohnwu.magisk.core.model.su.toPolicy import com.topjohnwu.magisk.core.model.su.toPolicy
import com.topjohnwu.magisk.di.AppContext import com.topjohnwu.magisk.di.AppContext
import com.topjohnwu.magisk.ktx.now import com.topjohnwu.magisk.ktx.now
@@ -55,13 +53,9 @@ class PolicyDao : BaseDao() {
private fun Map<String, String>.toPolicyOrNull(): SuPolicy? { private fun Map<String, String>.toPolicyOrNull(): SuPolicy? {
return runCatching { toPolicy(AppContext.packageManager) }.getOrElse { return runCatching { toPolicy(AppContext.packageManager) }.getOrElse {
Timber.e(it) Timber.w(it)
if (it is PackageManager.NameNotFoundException) { val uid = getOrElse("uid") { return null }
val uid = getOrElse("uid") { null } ?: return null GlobalScope.launch { delete(uid.toInt()) }
GlobalScope.launch {
delete(uid.toInt())
}
}
null null
} }
} }

View File

@@ -28,18 +28,10 @@ data class StubJson(
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ModuleJson( data class ModuleJson(
val id: String, val version: String,
val last_update: Long, val versionCode: Int,
val prop_url: String, val zipUrl: String,
val zip_url: String, val changelog: String,
val notes_url: String
)
@JsonClass(generateAdapter = true)
data class RepoJson(
val name: String,
val last_update: Long,
val modules: List<ModuleJson>
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View File

@@ -1,25 +1,43 @@
package com.topjohnwu.magisk.core.model.module package com.topjohnwu.magisk.core.model.module
import com.squareup.moshi.JsonDataException
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFile
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.IOException
import java.util.*
data class LocalModule(
private val path: String,
) : Module() {
private val svc get() = ServiceLocator.networkService
class LocalModule(path: String) : Module() {
override var id: String = "" override var id: String = ""
override var name: String = "" override var name: String = ""
override var author: String = ""
override var version: String = "" override var version: String = ""
override var versionCode: Int = -1 override var versionCode: Int = -1
override var description: String = "" var author: String = ""
var description: String = ""
var updateInfo: OnlineModule? = null
var outdated = false
private var updateUrl: String = ""
private val removeFile = SuFile(path, "remove") private val removeFile = SuFile(path, "remove")
private val disableFile = SuFile(path, "disable") private val disableFile = SuFile(path, "disable")
private val updateFile = SuFile(path, "update") private val updateFile = SuFile(path, "update")
private val ruleFile = SuFile(path, "sepolicy.rule") private val ruleFile = SuFile(path, "sepolicy.rule")
private val riruFolder = SuFile(path, "riru")
private val zygiskFolder = SuFile(path, "zygisk")
private val unloaded = SuFile(zygiskFolder, "unloaded")
val updated: Boolean get() = updateFile.exists() val updated: Boolean get() = updateFile.exists()
val isRiru: Boolean get() = (id == "riru-core") || riruFolder.exists()
val isZygisk: Boolean get() = zygiskFolder.exists()
val zygiskUnloaded: Boolean get() = unloaded.exists()
var enable: Boolean var enable: Boolean
get() = !disableFile.exists() get() = !disableFile.exists()
@@ -44,13 +62,14 @@ class LocalModule(path: String) : Module() {
get() = removeFile.exists() get() = removeFile.exists()
set(remove) { set(remove) {
if (remove) { if (remove) {
if (updateFile.exists()) return
removeFile.createNewFile() removeFile.createNewFile()
if (Const.Version.atLeast_21_2()) if (Const.Version.atLeast_21_2())
Shell.su("copy_sepolicy_rules").submit() Shell.su("copy_sepolicy_rules").submit()
else else
Shell.su("rm -rf $PERSIST/$id").submit() Shell.su("rm -rf $PERSIST/$id").submit()
} else { } else {
!removeFile.delete() removeFile.delete()
if (Const.Version.atLeast_21_2()) if (Const.Version.atLeast_21_2())
Shell.su("copy_sepolicy_rules").submit() Shell.su("copy_sepolicy_rules").submit()
else else
@@ -58,6 +77,30 @@ class LocalModule(path: String) : Module() {
} }
} }
@Throws(NumberFormatException::class)
private fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
"updateJson" -> updateUrl = value
}
}
}
init { init {
runCatching { runCatching {
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out) parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
@@ -73,17 +116,35 @@ class LocalModule(path: String) : Module() {
} }
} }
suspend fun fetch(): Boolean {
if (updateUrl.isEmpty())
return false
try {
val json = svc.fetchModuleJson(updateUrl)
updateInfo = OnlineModule(this, json)
outdated = json.versionCode > versionCode
return true
} catch (e: IOException) {
Timber.w(e)
} catch (e: JsonDataException) {
Timber.w(e)
}
return false
}
companion object { companion object {
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk" private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
suspend fun installed() = withContext(Dispatchers.IO) { suspend fun installed() = withContext(Dispatchers.IO) {
SuFile(Const.MAGISK_PATH) SuFile(Const.MAGISK_PATH)
.listFiles { _, name -> name != "lost+found" && name != ".core" } .listFiles()
.orEmpty() .orEmpty()
.filter { !it.isFile } .filter { !it.isFile }
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") } .map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
.sortedBy { it.name.toLowerCase() } .sortedBy { it.name.lowercase(Locale.ROOT) }
} }
} }
} }

View File

@@ -5,37 +5,10 @@ abstract class Module : Comparable<Module> {
protected set protected set
abstract var name: String abstract var name: String
protected set protected set
abstract var author: String
protected set
abstract var version: String abstract var version: String
protected set protected set
abstract var versionCode: Int abstract var versionCode: Int
protected set protected set
abstract var description: String
protected set
@Throws(NumberFormatException::class) override operator fun compareTo(other: Module) = id.compareTo(other.id)
protected fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
}
}
}
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
} }

View File

@@ -1,65 +1,27 @@
package com.topjohnwu.magisk.core.model.module package com.topjohnwu.magisk.core.model.module
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.model.ModuleJson import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.legalFilename
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import java.text.DateFormat
import java.util.*
@Entity(tableName = "modules")
@Parcelize @Parcelize
data class OnlineModule( data class OnlineModule(
@PrimaryKey override var id: String, override var id: String,
override var name: String = "", override var name: String,
override var author: String = "", override var version: String,
override var version: String = "", override var versionCode: Int,
override var versionCode: Int = -1, val zipUrl: String,
override var description: String = "", val changelog: String,
val last_update: Long,
val prop_url: String,
val zip_url: String,
val notes_url: String
) : Module(), Parcelable { ) : Module(), Parcelable {
constructor(local: LocalModule, json: ModuleJson) :
this(local.id, local.name, json.version, json.versionCode, json.zipUrl, json.changelog)
private val svc get() = ServiceLocator.networkService
constructor(info: ModuleJson) : this(
id = info.id,
last_update = info.last_update,
prop_url = info.prop_url,
zip_url = info.zip_url,
notes_url = info.notes_url
)
val lastUpdate get() = Date(last_update)
val lastUpdateString get() = DATE_FORMAT.format(lastUpdate)
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename() val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
suspend fun notes() = svc.fetchString(notes_url) private fun String.legalFilename() = replace(" ", "_")
.replace("'", "").replace("\"", "")
@Throws(IllegalRepoException::class) .replace("$", "").replace("`", "")
suspend fun load() { .replace("*", "").replace("/", "_")
try { .replace("#", "").replace("@", "")
val rawProps = svc.fetchString(prop_url) .replace("\\", "_")
val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
parseProps(props)
} catch (e: Exception) {
throw IllegalRepoException("Repo [$id] parse error:", e)
}
if (versionCode < 0) {
throw IllegalRepoException("Repo [$id] does not contain versionCode")
}
}
class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause)
companion object {
private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
}
} }

View File

@@ -1,12 +1,8 @@
package com.topjohnwu.magisk.core.model.su package com.topjohnwu.magisk.core.model.su
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.ALLOW
import com.topjohnwu.magisk.ktx.now import com.topjohnwu.magisk.ktx.now
import com.topjohnwu.magisk.ktx.timeFormatTime
import com.topjohnwu.magisk.ktx.toTime
@Entity(tableName = "logs") @Entity(tableName = "logs")
data class SuLog( data class SuLog(
@@ -17,14 +13,7 @@ data class SuLog(
val appName: String, val appName: String,
val command: String, val command: String,
val action: Boolean, val action: Boolean,
val time: Long = -1 val time: Long = now
) { ) {
@PrimaryKey(autoGenerate = true) var id: Int = 0 @PrimaryKey(autoGenerate = true) var id: Int = 0
@Ignore val timeString = time.toTime(timeFormatTime)
} }
fun SuPolicy.toLog(
toUid: Int,
fromPid: Int,
command: String
) = SuLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)

View File

@@ -9,7 +9,7 @@ import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
import com.topjohnwu.magisk.ktx.getLabel import com.topjohnwu.magisk.ktx.getLabel
data class SuPolicy( data class SuPolicy(
var uid: Int, val uid: Int,
val packageName: String, val packageName: String,
val appName: String, val appName: String,
val icon: Drawable, val icon: Drawable,
@@ -25,16 +25,19 @@ data class SuPolicy(
const val ALLOW = 2 const val ALLOW = 2
} }
} fun toLog(toUid: Int, fromPid: Int, command: String) = SuLog(
uid, toUid, fromPid, packageName, appName,
command, policy == ALLOW)
fun SuPolicy.toMap() = mapOf( fun toMap() = mapOf(
"uid" to uid, "uid" to uid,
"package_name" to packageName, "package_name" to packageName,
"policy" to policy, "policy" to policy,
"until" to until, "until" to until,
"logging" to logging, "logging" to logging,
"notification" to notification "notification" to notification
) )
}
@Throws(PackageManager.NameNotFoundException::class) @Throws(PackageManager.NameNotFoundException::class)
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy { fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
@@ -70,3 +73,13 @@ fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
policy = policy policy = policy
) )
} }
fun Int.toUidPolicy(pm: PackageManager, policy: Int): SuPolicy {
return SuPolicy(
uid = this,
packageName = "[UID] $this",
appName = "[UID] $this",
icon = pm.defaultActivityIcon,
policy = policy
)
}

View File

@@ -1,24 +1,16 @@
package com.topjohnwu.magisk.core.su package com.topjohnwu.magisk.core.su
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Process
import android.widget.Toast import android.widget.Toast
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.toLog
import com.topjohnwu.magisk.core.model.su.toPolicy import com.topjohnwu.magisk.core.model.su.toPolicy
import com.topjohnwu.magisk.core.model.su.toUidPolicy
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.startActivity
import com.topjohnwu.magisk.ktx.startActivityWithRoot
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@@ -28,9 +20,8 @@ object SuCallbackHandler {
const val REQUEST = "request" const val REQUEST = "request"
const val LOG = "log" const val LOG = "log"
const val NOTIFY = "notify" const val NOTIFY = "notify"
const val TEST = "test"
operator fun invoke(context: Context, action: String?, data: Bundle?) { fun run(context: Context, action: String?, data: Bundle?) {
data ?: return data ?: return
// Debug messages // Debug messages
@@ -44,59 +35,42 @@ object SuCallbackHandler {
} }
when (action) { when (action) {
REQUEST -> handleRequest(context, data)
LOG -> handleLogging(context, data) LOG -> handleLogging(context, data)
NOTIFY -> handleNotify(context, data) NOTIFY -> handleNotify(context, data)
TEST -> {
val mode = data.getInt("mode", 2)
Shell.su(
"magisk --connect-mode $mode",
"magisk --use-broadcast"
).submit()
}
} }
} }
private fun Any?.toInt(): Int? { // https://android.googlesource.com/platform/frameworks/base/+/547bf5487d52b93c9fe183aa6d56459c170b17a4
return when (this) { private fun Bundle.getIntComp(key: String, defaultValue: Int): Int {
is Number -> this.toInt() val value = get(key) ?: return defaultValue
else -> null return when (value) {
} is Int -> value
} is Long -> value.toInt()
else -> defaultValue
private fun handleRequest(context: Context, data: Bundle) {
val intent = context.intent<SuRequestActivity>()
.setAction(REQUEST)
.putExtras(data)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
if (Build.VERSION.SDK_INT >= 29) {
// Android Q does not allow starting activity from background
intent.startActivityWithRoot()
} else {
intent.startActivity(context)
} }
} }
private fun handleLogging(context: Context, data: Bundle) { private fun handleLogging(context: Context, data: Bundle) {
val fromUid = data["from.uid"].toInt() ?: return val fromUid = data.getIntComp("from.uid", -1)
if (fromUid == Process.myUid()) val notify = data.getBoolean("notify", true)
return val allow = data.getIntComp("policy", SuPolicy.ALLOW)
val pm = context.packageManager val pm = context.packageManager
val notify = data.getBoolean("notify", true) val policy = runCatching {
val allow = data["policy"].toInt() ?: return fromUid.toPolicy(pm, allow)
}.getOrElse {
val policy = runCatching { fromUid.toPolicy(pm, allow) }.getOrElse { return } GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
fromUid.toUidPolicy(pm, allow)
}
if (notify) if (notify)
notify(context, policy) notify(context, policy)
val toUid = data["to.uid"].toInt() ?: return val toUid = data.getIntComp("to.uid", -1)
val pid = data["pid"].toInt() ?: return val pid = data.getIntComp("pid", -1)
val command = data.getString("command") ?: return val command = data.getString("command", "")
val log = policy.toLog( val log = policy.toLog(
toUid = toUid, toUid = toUid,
fromPid = pid, fromPid = pid,
@@ -109,22 +83,23 @@ object SuCallbackHandler {
} }
private fun handleNotify(context: Context, data: Bundle) { private fun handleNotify(context: Context, data: Bundle) {
val fromUid = data["from.uid"].toInt() ?: return val fromUid = data.getIntComp("from.uid", -1)
if (fromUid == Process.myUid()) val allow = data.getIntComp("policy", SuPolicy.ALLOW)
return
val pm = context.packageManager val pm = context.packageManager
val allow = data["policy"].toInt() ?: return
runCatching {
val policy = fromUid.toPolicy(pm, allow) val policy = runCatching {
if (policy.policy >= 0) fromUid.toPolicy(pm, allow)
notify(context, policy) }.getOrElse {
GlobalScope.launch { ServiceLocator.policyDB.delete(fromUid) }
fromUid.toUidPolicy(pm, allow)
} }
notify(context, policy)
} }
private fun notify(context: Context, policy: SuPolicy) { private fun notify(context: Context, policy: SuPolicy) {
if (policy.notification && Config.suNotification == Config.Value.NOTIFICATION_TOAST) { if (Config.suNotification == Config.Value.NOTIFICATION_TOAST) {
val resId = if (policy.policy == SuPolicy.ALLOW) val resId = if (policy.policy == SuPolicy.ALLOW)
R.string.su_allow_toast R.string.su_allow_toast
else else

View File

@@ -2,21 +2,23 @@ package com.topjohnwu.magisk.core.su
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.LocalSocket
import android.net.LocalSocketAddress
import androidx.collection.ArrayMap
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.model.su.SuPolicy import com.topjohnwu.magisk.core.model.su.SuPolicy
import com.topjohnwu.magisk.core.model.su.toPolicy import com.topjohnwu.magisk.core.model.su.toPolicy
import com.topjohnwu.magisk.ktx.now import com.topjohnwu.magisk.ktx.now
import kotlinx.coroutines.* import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.* import java.io.Closeable
import java.io.DataOutputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.SECONDS
class SuRequestHandler( class SuRequestHandler(
private val pm: PackageManager, private val pm: PackageManager,
@@ -33,8 +35,10 @@ class SuRequestHandler(
return false return false
// Never allow com.topjohnwu.magisk (could be malware) // Never allow com.topjohnwu.magisk (could be malware)
if (policy.packageName == BuildConfig.APPLICATION_ID) if (policy.packageName == BuildConfig.APPLICATION_ID) {
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
return false return false
}
when (Config.suAutoResponse) { when (Config.suAutoResponse) {
Config.Value.SU_AUTO_DENY -> { Config.Value.SU_AUTO_DENY -> {
@@ -50,12 +54,6 @@ class SuRequestHandler(
return true return true
} }
private suspend fun <T> Deferred<T>.timedAwait() : T? {
return withTimeoutOrNull(SECONDS.toMillis(1)) {
await()
}
}
@Throws(IOException::class) @Throws(IOException::class)
override fun close() { override fun close() {
if (::output.isInitialized) if (::output.isInitialized)
@@ -66,20 +64,9 @@ class SuRequestHandler(
private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) { private suspend fun init(intent: Intent) = withContext(Dispatchers.IO) {
try { try {
val uid: Int val name = intent.getStringExtra("fifo") ?: throw SuRequestError()
if (Const.Version.atLeast_21_0()) { val uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
val name = intent.getStringExtra("fifo") ?: throw SuRequestError() output = DataOutputStream(FileOutputStream(name).buffered())
uid = intent.getIntExtra("uid", -1).also { if (it < 0) throw SuRequestError() }
output = DataOutputStream(FileOutputStream(name).buffered())
} else {
val name = intent.getStringExtra("socket") ?: throw SuRequestError()
val socket = LocalSocket()
socket.connect(LocalSocketAddress(name, LocalSocketAddress.Namespace.ABSTRACT))
output = DataOutputStream(BufferedOutputStream(socket.outputStream))
val input = DataInputStream(BufferedInputStream(socket.inputStream))
val map = async { input.readRequest() }.timedAwait() ?: throw SuRequestError()
uid = map["uid"]?.toIntOrNull() ?: throw SuRequestError()
}
policy = uid.toPolicy(pm) policy = uid.toPolicy(pm)
true true
} catch (e: Exception) { } catch (e: Exception) {
@@ -102,7 +89,6 @@ class SuRequestHandler(
policy.policy = action policy.policy = action
policy.until = until policy.until = until
policy.uid = policy.uid % 100000 + Const.USER_ID * 100000
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
try { try {
@@ -117,23 +103,4 @@ class SuRequestHandler(
} }
} }
} }
@Throws(IOException::class)
private fun DataInputStream.readRequest(): Map<String, String> {
fun readString(): String {
val len = readInt()
val buf = ByteArray(len)
readFully(buf)
return String(buf, Charsets.UTF_8)
}
val ret = ArrayMap<String, String>()
while (true) {
val name = readString()
if (name == "eof")
break
ret[name] = readString()
}
return ret
}
} }

View File

@@ -1,7 +1,6 @@
package com.topjohnwu.magisk.core.tasks package com.topjohnwu.magisk.core.tasks
import android.app.Activity import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.widget.Toast import android.widget.Toast
@@ -16,10 +15,10 @@ import com.topjohnwu.magisk.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.signing.JarMap
import com.topjohnwu.magisk.signing.SignApk
import com.topjohnwu.magisk.utils.APKInstall import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignApk
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -27,7 +26,6 @@ import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference
import java.security.SecureRandom import java.security.SecureRandom
object HideAPK { object HideAPK {
@@ -41,8 +39,6 @@ object HideAPK {
const val MAX_LABEL_LENGTH = 32 const val MAX_LABEL_LENGTH = 32
private val svc get() = ServiceLocator.networkService private val svc get() = ServiceLocator.networkService
private val Context.APK_URI get() = Provider.APK_URI(packageName)
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
private fun genPackageName(): String { private fun genPackageName(): String {
val random = SecureRandom() val random = SecureRandom()
@@ -92,35 +88,16 @@ object HideAPK {
return true return true
} }
private class WaitPackageReceiver( private fun launchApp(activity: Activity, pkg: String) {
private val pkg: String, val intent = activity.packageManager.getLaunchIntentForPackage(pkg) ?: return
activity: Activity Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
) : BroadcastReceiver() { val self = activity.packageName
val flag = Intent.FLAG_GRANT_READ_URI_PERMISSION
private val activity = WeakReference(activity) activity.grantUriPermission(pkg, Provider.APK_URI(self), flag)
activity.grantUriPermission(pkg, Provider.PREFS_URI(self), flag)
private fun launchApp(): Unit = activity.get()?.run { intent.putExtra(Const.Key.PREV_PKG, self)
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return activity.startActivity(intent)
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg activity.finish()
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.putExtra(Const.Key.PREV_PKG, packageName)
startActivity(intent)
finish()
} ?: Unit
override fun onReceive(context: Context, intent: Intent) {
when (intent.action ?: return) {
Intent.ACTION_PACKAGE_REPLACED, Intent.ACTION_PACKAGE_ADDED -> {
val newPkg = intent.data?.encodedSchemeSpecificPart.orEmpty()
if (newPkg == pkg) {
context.unregisterReceiver(this)
launchApp()
}
}
}
}
} }
private suspend fun patchAndHide(activity: Activity, label: String): Boolean { private suspend fun patchAndHide(activity: Activity, label: String): Boolean {
@@ -129,7 +106,9 @@ object HideAPK {
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub) svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
} catch (e: IOException) { } catch (e: IOException) {
Timber.e(e) Timber.e(e)
return false stub.createNewFile()
val cmd = "\$MAGISKBIN/magiskinit -x manager ${stub.path}"
if (!Shell.su(cmd).exec().isSuccess) return false
} }
// Generate a new random package name and signature // Generate a new random package name and signature
@@ -141,27 +120,52 @@ object HideAPK {
return false return false
// Install and auto launch app // Install and auto launch app
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(pkg, activity)) val receiver = APKInstall.register(activity, pkg) {
if (!Shell.su("adb_pm_install $repack").exec().isSuccess) launchApp(activity, pkg)
APKInstall.installHideResult(activity, repack) }
val cmd = "adb_pm_install $repack ${activity.applicationInfo.uid}"
if (!Shell.su(cmd).exec().isSuccess) {
APKInstall.installapk(activity, repack)
receiver.waitIntent()?.let { activity.startActivity(it) }
}
return true return true
} }
@Suppress("DEPRECATION")
suspend fun hide(activity: Activity, label: String) { suspend fun hide(activity: Activity, label: String) {
val dialog = android.app.ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.hide_app_title))
isIndeterminate = true
setCancelable(false)
show()
}
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
patchAndHide(activity, label) patchAndHide(activity, label)
} }
if (!result) { if (!result) {
dialog.dismiss()
Utils.toast(R.string.failure, Toast.LENGTH_LONG) Utils.toast(R.string.failure, Toast.LENGTH_LONG)
} }
} }
@Suppress("DEPRECATION")
fun restore(activity: Activity) { fun restore(activity: Activity) {
val dialog = android.app.ProgressDialog(activity).apply {
setTitle(activity.getString(R.string.restore_img_msg))
isIndeterminate = true
setCancelable(false)
show()
}
val apk = DynAPK.current(activity) val apk = DynAPK.current(activity)
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(APPLICATION_ID, activity)) val receiver = APKInstall.register(activity, APPLICATION_ID) {
Shell.su("adb_pm_install $apk").submit { launchApp(activity, APPLICATION_ID)
if (!it.isSuccess) dialog.dismiss()
APKInstall.installHideResult(activity, apk) }
val cmd = "adb_pm_install $apk ${activity.applicationInfo.uid}"
Shell.su(cmd).submit(Shell.EXECUTOR) { ret ->
if (ret.isSuccess) return@submit
APKInstall.installapk(activity, apk)
receiver.waitIntent()?.let { activity.startActivity(it) }
} }
} }
} }

View File

@@ -16,8 +16,8 @@ import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.reboot import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.ktx.withStreams import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.signing.SignBoot
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.NOPList import com.topjohnwu.superuser.internal.NOPList
@@ -37,6 +37,7 @@ import java.io.*
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.SecureRandom import java.security.SecureRandom
import java.util.* import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
abstract class MagiskInstallImpl protected constructor( abstract class MagiskInstallImpl protected constructor(
@@ -93,8 +94,14 @@ abstract class MagiskInstallImpl protected constructor(
// Extract binaries // Extract binaries
if (isRunningAsStub) { if (isRunningAsStub) {
val zf = ZipFile(DynAPK.current(context)) val zf = ZipFile(DynAPK.current(context))
// Also extract magisk32 on non 64-bit only 64-bit devices
val is32lib = Const.CPU_ABI_32?.let {
{ entry: ZipEntry -> entry.name == "lib/$it/libmagisk32.so" }
} ?: { false }
zf.entries().asSequence().filter { zf.entries().asSequence().filter {
!it.isDirectory && it.name.startsWith("lib/${Const.CPU_ABI_32}/") !it.isDirectory && (it.name.startsWith("lib/${Const.CPU_ABI}/") || is32lib(it))
}.forEach { }.forEach {
val n = it.name.substring(it.name.lastIndexOf('/') + 1) val n = it.name.substring(it.name.lastIndexOf('/') + 1)
val name = n.substring(3, n.length - 3) val name = n.substring(3, n.length - 3)
@@ -102,9 +109,17 @@ abstract class MagiskInstallImpl protected constructor(
zf.getInputStream(it).writeTo(dest) zf.getInputStream(it).writeTo(dest)
} }
} else { } else {
val libs = Const.NATIVE_LIB_DIR.listFiles { _, name -> val info = context.applicationInfo
var libs = File(info.nativeLibraryDir).listFiles { _, name ->
name.startsWith("lib") && name.endsWith(".so") name.startsWith("lib") && name.endsWith(".so")
} ?: emptyArray() } ?: emptyArray()
// Also symlink magisk32 on non 64-bit only 64-bit devices
val lib32 = info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
if (lib32 != null) {
libs += File(lib32, "libmagisk32.so")
}
for (lib in libs) { for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3) val name = lib.name.substring(3, lib.name.length - 3)
Os.symlink(lib.path, "$installDir/$name") Os.symlink(lib.path, "$installDir/$name")
@@ -250,7 +265,7 @@ abstract class MagiskInstallImpl protected constructor(
src.reset() src.reset()
val alpha = "abcdefghijklmnopqrstuvwxyz" val alpha = "abcdefghijklmnopqrstuvwxyz"
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789" val alphaNum = "$alpha${alpha.uppercase(Locale.ROOT)}0123456789"
val random = SecureRandom() val random = SecureRandom()
val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run { val filename = StringBuilder("magisk_patched-${BuildConfig.VERSION_CODE}_").run {
for (i in 1..5) { for (i in 1..5) {
@@ -309,11 +324,7 @@ abstract class MagiskInstallImpl protected constructor(
// Fix up binaries // Fix up binaries
srcBoot.delete() srcBoot.delete()
if (shell.isRoot) { "cp_readlink $installDir".sh()
"fix_env $installDir".sh()
} else {
"cp_readlink $installDir".sh()
}
return true return true
} }
@@ -346,6 +357,7 @@ abstract class MagiskInstallImpl protected constructor(
"cd $installDir", "cd $installDir",
"KEEPFORCEENCRYPT=${Config.keepEnc} " + "KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " + "KEEPVERITY=${Config.keepVerity} " +
"PATCHVBMETAFLAG=${Config.patchVbmeta} " +
"RECOVERYMODE=${Config.recovery} " + "RECOVERYMODE=${Config.recovery} " +
"sh boot_patch.sh $srcBoot") "sh boot_patch.sh $srcBoot")
@@ -376,10 +388,10 @@ abstract class MagiskInstallImpl protected constructor(
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
private suspend fun postOTA(): Boolean { private fun postOTA(): Boolean {
try { try {
val bootctl = File.createTempFile("bootctl", null, context.cacheDir) val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
service.fetchBootctl().byteStream().writeTo(bootctl) context.assets.open("bootctl").writeTo(bootctl)
"post_ota $bootctl".sh() "post_ota $bootctl".sh()
} catch (e: IOException) { } catch (e: IOException) {
console.add("! Unable to download bootctl") console.add("! Unable to download bootctl")
@@ -407,7 +419,7 @@ abstract class MagiskInstallImpl protected constructor(
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
protected fun uninstall() = "run_uninstaller ${AssetHack.apk}".sh().isSuccess protected fun uninstall() = "run_uninstaller $AppApkPath".sh().isSuccess
@WorkerThread @WorkerThread
protected abstract suspend fun operations(): Boolean protected abstract suspend fun operations(): Boolean

View File

@@ -1,42 +0,0 @@
package com.topjohnwu.magisk.core.tasks
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.synchronized
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.util.*
class RepoUpdater(
private val svc: NetworkService,
private val repoDB: RepoDao
) {
suspend fun run(forced: Boolean) = withContext(Dispatchers.IO) {
val cachedMap = HashMap<String, Date>().also { map ->
repoDB.getModuleStubs().forEach { map[it.id] = Date(it.last_update) }
}.synchronized()
svc.fetchRepoInfo()?.let { info ->
coroutineScope {
info.modules.forEach {
launch {
val lastUpdated = cachedMap.remove(it.id)
if (forced || lastUpdated?.before(Date(it.last_update)) != false) {
try {
val repo = OnlineModule(it).apply { load() }
repoDB.addModule(repo)
} catch (e: OnlineModule.IllegalRepoException) {
Timber.e(e)
}
}
}
}
}
repoDB.removeModules(cachedMap.keys)
}
}
}

View File

@@ -1,13 +1,19 @@
package com.topjohnwu.magisk.core.utils package com.topjohnwu.magisk.core.utils
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.concurrent.* import java.util.concurrent.AbstractExecutorService
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class IODispatcherExecutor : AbstractExecutorService() { class DispatcherExecutor(dispatcher: CoroutineDispatcher) : AbstractExecutorService() {
private val job = SupervisorJob().apply { invokeOnCompletion { future.run() } } private val job = SupervisorJob()
private val scope = CoroutineScope(job + Dispatchers.IO) private val scope = CoroutineScope(job + dispatcher)
private val future = FutureTask(Callable { true }) private val latch = CountDownLatch(1)
init {
job.invokeOnCompletion { latch.countDown() }
}
override fun execute(command: Runnable) { override fun execute(command: Runnable) {
scope.launch { scope.launch {
@@ -26,11 +32,5 @@ class IODispatcherExecutor : AbstractExecutorService() {
override fun isTerminated() = job.isCancelled && job.isCompleted override fun isTerminated() = job.isCancelled && job.isCompleted
override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean { override fun awaitTermination(timeout: Long, unit: TimeUnit) = latch.await(timeout, unit)
return try {
future.get(timeout, unit)
} catch (e: TimeoutException) {
false
}
}
} }

View File

@@ -5,9 +5,9 @@ import android.content.pm.PackageManager
import android.util.Base64 import android.util.Base64
import android.util.Base64OutputStream import android.util.Base64OutputStream
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.signing.CryptoUtils.readCertificate import com.topjohnwu.magisk.signing.CryptoUtils.readCertificate
import com.topjohnwu.signing.CryptoUtils.readPrivateKey import com.topjohnwu.magisk.signing.CryptoUtils.readPrivateKey
import com.topjohnwu.signing.KeyData import com.topjohnwu.magisk.signing.KeyData
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
@@ -41,7 +41,7 @@ class Keygen(context: Context) : CertKeyProvider {
} }
private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) } private val start = Calendar.getInstance().apply { add(Calendar.MONTH, -3) }
private val end = start.apply { add(Calendar.YEAR, 30) } private val end = (start.clone() as Calendar).apply { add(Calendar.YEAR, 30) }
override val cert get() = provider.cert override val cert get() = provider.cert
override val key get() = provider.key override val key get() = provider.key

View File

@@ -6,8 +6,9 @@ import android.annotation.SuppressLint
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.AssetHack
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.createNewResources
import com.topjohnwu.magisk.di.AppContext
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.*
@@ -25,7 +26,7 @@ withContext(Dispatchers.Default) {
val compareId = R.string.app_changelog val compareId = R.string.app_changelog
// Create a completely new resource to prevent cross talk over active configs // Create a completely new resource to prevent cross talk over active configs
val res = AssetHack.newResource() val res = createNewResources()
val locales = ArrayList<String>().apply { val locales = ArrayList<String>().apply {
// Add default locale // Add default locale
@@ -40,13 +41,13 @@ withContext(Dispatchers.Default) {
}.map { }.map {
Locale.forLanguageTag(it) Locale.forLanguageTag(it)
}.distinctBy { }.distinctBy {
res.updateLocale(it) res.setLocale(it)
res.getString(compareId) res.getString(compareId)
}.sortedWith { a, b -> }.sortedWith { a, b ->
a.getDisplayName(a).compareTo(b.getDisplayName(b), true) a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
} }
res.updateLocale(defaultLocale) res.setLocale(defaultLocale)
val defName = res.getString(R.string.system_default) val defName = res.getString(R.string.system_default)
val names = ArrayList<String>(locales.size + 1) val names = ArrayList<String>(locales.size + 1)
@@ -63,12 +64,14 @@ withContext(Dispatchers.Default) {
(names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it } (names.toTypedArray() to values.toTypedArray()).also { cachedLocales = it }
} }
fun Resources.updateConfig(config: Configuration = configuration) { fun Resources.setConfig(config: Configuration) {
config.setLocale(currentLocale) config.setLocale(currentLocale)
updateConfiguration(config, displayMetrics) updateConfiguration(config, displayMetrics)
} }
fun Resources.updateLocale(locale: Locale) { fun Resources.syncLocale() = setConfig(configuration)
fun Resources.setLocale(locale: Locale) {
configuration.setLocale(locale) configuration.setLocale(locale)
updateConfiguration(configuration, displayMetrics) updateConfiguration(configuration, displayMetrics)
} }
@@ -80,5 +83,5 @@ fun refreshLocale() {
else -> Locale.forLanguageTag(localeConfig) else -> Locale.forLanguageTag(localeConfig)
} }
Locale.setDefault(currentLocale) Locale.setDefault(currentLocale)
AssetHack.resource.updateConfig() AppContext.resources.syncLocale()
} }

View File

@@ -5,7 +5,7 @@ import java.io.InputStream
class ProgressInputStream( class ProgressInputStream(
base: InputStream, base: InputStream,
val progressEmitter: (Long) -> Unit = {} val progressEmitter: (Long) -> Unit
) : FilterInputStream(base) { ) : FilterInputStream(base) {
private var bytesRead = 0L private var bytesRead = 0L
@@ -40,4 +40,9 @@ class ProgressInputStream(
} }
return sz return sz
} }
override fun close() {
super.close()
progressEmitter(bytesRead)
}
} }

View File

@@ -0,0 +1,55 @@
package com.topjohnwu.magisk.core.utils
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Binder
import android.os.IBinder
import com.topjohnwu.superuser.ipc.RootService
import timber.log.Timber
import java.util.concurrent.CountDownLatch
import kotlin.system.exitProcess
class RootRegistry(stub: Any?) : RootService() {
constructor() : this(null)
private val className: String? = stub?.javaClass?.name
init {
// Always log full stack trace with Timber
Timber.plant(Timber.DebugTree())
Thread.setDefaultUncaughtExceptionHandler { _, e ->
Timber.e(e)
exitProcess(1)
}
}
override fun onBind(intent: Intent): IBinder {
// TODO: PLACEHOLDER
return Binder()
}
override fun getComponentName(): ComponentName {
return ComponentName(packageName, className ?: javaClass.name)
}
// TODO: PLACEHOLDER
object Connection : CountDownLatch(1), ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
Timber.d("onServiceConnected")
countDown()
}
override fun onNullBinding(name: ComponentName) {
Timber.d("onServiceConnected")
countDown()
}
override fun onServiceDisconnected(name: ComponentName) {
bind(Intent().setComponent(name), this)
}
}
companion object {
var bindTask: Runnable? = null
}
}

View File

@@ -1,10 +1,12 @@
package com.topjohnwu.magisk.core.utils package com.topjohnwu.magisk.core.utils
import android.content.Context import android.content.Context
import android.os.Build
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.* import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.ktx.cachedFile import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.deviceProtectedContext import com.topjohnwu.magisk.ktx.deviceProtectedContext
import com.topjohnwu.magisk.ktx.rawResource import com.topjohnwu.magisk.ktx.rawResource
@@ -14,18 +16,12 @@ import com.topjohnwu.superuser.ShellUtils
import java.io.File import java.io.File
import java.util.jar.JarFile import java.util.jar.JarFile
abstract class BaseShellInit : Shell.Initializer() { class ShellInit : Shell.Initializer() {
final override fun onInit(context: Context, shell: Shell): Boolean { override fun onInit(context: Context, shell: Shell): Boolean {
return init(context.wrap(), shell) if (shell.isRoot) {
} RootRegistry.bindTask?.run()
RootRegistry.bindTask = null
abstract fun init(context: Context, shell: Shell): Boolean }
}
class BusyBoxInit : BaseShellInit() {
override fun init(context: Context, shell: Shell): Boolean {
shell.newJob().apply { shell.newJob().apply {
add("export ASH_STANDALONE=1") add("export ASH_STANDALONE=1")
@@ -34,13 +30,13 @@ class BusyBoxInit : BaseShellInit() {
if (!shell.isRoot) if (!shell.isRoot)
return true return true
val jar = JarFile(DynAPK.current(context)) val jar = JarFile(DynAPK.current(context))
val bb = jar.getJarEntry("lib/${Const.CPU_ABI_32}/libbusybox.so") val bb = jar.getJarEntry("lib/${Const.CPU_ABI}/libbusybox.so")
localBB = context.deviceProtectedContext.cachedFile("busybox") localBB = context.deviceProtectedContext.cachedFile("busybox")
localBB.delete() localBB.delete()
jar.getInputStream(bb).writeTo(localBB) jar.getInputStream(bb).writeTo(localBB)
localBB.setExecutable(true) localBB.setExecutable(true)
} else { } else {
localBB = File(Const.NATIVE_LIB_DIR, "libbusybox.so") localBB = File(context.applicationInfo.nativeLibraryDir, "libbusybox.so")
} }
if (shell.isRoot) { if (shell.isRoot) {
@@ -64,20 +60,7 @@ class BusyBoxInit : BaseShellInit() {
// Directly execute the file // Directly execute the file
add("exec $localBB sh") add("exec $localBB sh")
} }
}.exec()
return true
}
}
class AppShellInit : BaseShellInit() {
override fun init(context: Context, shell: Shell): Boolean {
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
fun getVar(name: String) = fastCmd("echo \$$name")
fun getBool(name: String) = getVar(name).toBoolean()
shell.newJob().apply {
add(context.rawResource(R.raw.manager)) add(context.rawResource(R.raw.manager))
if (shell.isRoot) { if (shell.isRoot) {
add(context.assets.open("util_functions.sh")) add(context.assets.open("util_functions.sh"))
@@ -85,9 +68,14 @@ class AppShellInit : BaseShellInit() {
add("app_init") add("app_init")
}.exec() }.exec()
fun fastCmd(cmd: String) = ShellUtils.fastCmd(shell, cmd)
fun getVar(name: String) = fastCmd("echo \$$name")
fun getBool(name: String) = getVar(name).toBoolean()
Const.MAGISKTMP = getVar("MAGISKTMP") Const.MAGISKTMP = getVar("MAGISKTMP")
Info.isSAR = getBool("SYSTEM_ROOT") Info.isSAR = getBool("SYSTEM_ROOT")
Info.ramdisk = getBool("RAMDISKEXIST") Info.ramdisk = getBool("RAMDISKEXIST")
Info.vbmeta = getBool("VBMETAEXIST")
Info.isAB = getBool("ISAB") Info.isAB = getBool("ISAB")
Info.crypto = getVar("CRYPTOTYPE") Info.crypto = getVar("CRYPTOTYPE")
@@ -95,6 +83,7 @@ class AppShellInit : BaseShellInit() {
Config.recovery = getBool("RECOVERYMODE") Config.recovery = getBool("RECOVERYMODE")
Config.keepVerity = getBool("KEEPVERITY") Config.keepVerity = getBool("KEEPVERITY")
Config.keepEnc = getBool("KEEPFORCEENCRYPT") Config.keepEnc = getBool("KEEPFORCEENCRYPT")
Config.patchVbmeta = getBool("PATCHVBMETAFLAG")
return true return true
} }

View File

@@ -38,8 +38,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
} }
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) } SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
} }
} catch (e: IOException) { } catch (e: IllegalArgumentException) {
e.printStackTrace() throw IOException(e)
throw e
} }
} }

View File

@@ -1,90 +0,0 @@
package com.topjohnwu.magisk.data.database
import androidx.room.*
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.module.OnlineModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Database(version = 8, entities = [OnlineModule::class], exportSchema = false)
abstract class RepoDatabase : RoomDatabase() {
abstract fun repoDao() : RepoDao
}
@Dao
abstract class RepoDao(private val db: RepoDatabase) {
suspend fun clear() = withContext(Dispatchers.IO) { db.clearAllTables() }
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun addModule(repo: OnlineModule)
@Delete
abstract fun removeModule(repo: OnlineModule)
@Query("DELETE FROM modules WHERE id = :id")
abstract fun removeModule(id: String)
@Query("DELETE FROM modules WHERE id IN (:idList)")
abstract fun removeModules(idList: Collection<String>)
@Query("SELECT * FROM modules WHERE id = :id")
abstract fun getModule(id: String): OnlineModule?
@Query("SELECT id, last_update FROM modules")
abstract fun getModuleStubs(): List<ModuleStub>
fun getModules(offset: Int, limit: Int = LIMIT) = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> getNameOrder(offset, limit)
else -> getDateOrder(offset, limit)
}
fun searchModules(query: String, offset: Int, limit: Int = LIMIT) = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> searchNameOrder(query, offset, limit)
else -> searchDateOrder(query, offset, limit)
}
@Query("SELECT * FROM modules WHERE id = :id AND versionCode > :versionCode LIMIT 1")
abstract fun getUpdatableModule(id: String, versionCode: Int): OnlineModule?
@Query("SELECT * FROM modules ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
protected abstract fun getDateOrder(offset: Int, limit: Int): List<OnlineModule>
@Query("SELECT * FROM modules ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
protected abstract fun getNameOrder(offset: Int, limit: Int): List<OnlineModule>
@Query(
"""SELECT *
FROM modules
WHERE
(author LIKE '%' || :query || '%') ||
(name LIKE '%' || :query || '%') ||
(description LIKE '%' || :query || '%')
ORDER BY last_update DESC
LIMIT :limit
OFFSET :offset"""
)
protected abstract fun searchDateOrder(query: String, offset: Int, limit: Int): List<OnlineModule>
@Query(
"""SELECT *
FROM modules
WHERE
(author LIKE '%' || :query || '%') ||
(name LIKE '%' || :query || '%') ||
(description LIKE '%' || :query || '%')
ORDER BY name COLLATE NOCASE
LIMIT :limit
OFFSET :offset"""
)
protected abstract fun searchNameOrder(query: String, offset: Int, limit: Int): List<OnlineModule>
companion object {
const val LIMIT = 10
}
}
data class ModuleStub(
@PrimaryKey val id: String,
val last_update: Long
)

View File

@@ -1,49 +1,26 @@
package com.topjohnwu.magisk.data.network package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.BranchInfo import com.topjohnwu.magisk.core.model.BranchInfo
import com.topjohnwu.magisk.core.model.RepoJson import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.http.* import retrofit2.http.*
private const val REVISION = "revision"
private const val BRANCH = "branch" private const val BRANCH = "branch"
private const val REPO = "repo" private const val REPO = "repo"
private const val FILE = "file" private const val FILE = "file"
const val MAGISK_FILES = "topjohnwu/magisk-files"
const val MAGISK_MAIN = "topjohnwu/Magisk"
interface GithubPageServices { interface GithubPageServices {
@GET("{$FILE}") @GET("{$FILE}")
suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo suspend fun fetchUpdateJSON(@Path(FILE) file: String): UpdateInfo
} }
interface JSDelivrServices {
@GET("$MAGISK_FILES@{$REVISION}/snet")
@Streaming
suspend fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): ResponseBody
@GET("$MAGISK_FILES@{$REVISION}/bootctl")
@Streaming
suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody
@GET("$MAGISK_MAIN@{$REVISION}/scripts/module_installer.sh")
@Streaming
suspend fun fetchInstaller(@Path(REVISION) revision: String): ResponseBody
}
interface RawServices { interface RawServices {
@GET @GET
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
@GET
suspend fun fetchRepoInfo(@Url url: String): RepoJson
@GET @GET
@Streaming @Streaming
suspend fun fetchFile(@Url url: String): ResponseBody suspend fun fetchFile(@Url url: String): ResponseBody
@@ -51,6 +28,9 @@ interface RawServices {
@GET @GET
suspend fun fetchString(@Url url: String): String suspend fun fetchString(@Url url: String): String
@GET
suspend fun fetchModuleJson(@Url url: String): ModuleJson
} }
interface GithubApiServices { interface GithubApiServices {
@@ -62,4 +42,3 @@ interface GithubApiServices {
@Path(BRANCH) branch: String @Path(BRANCH) branch: String
): BranchInfo ): BranchInfo
} }

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.ktx.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class BooleanProperty(
private val name: String,
private val default: Boolean,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Boolean {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Boolean
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.ktx.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class FloatProperty(
private val name: String,
private val default: Float,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Float {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Float
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.ktx.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class IntProperty(
private val name: String,
private val default: Int,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Int {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Int
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.ktx.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LongProperty(
private val name: String,
private val default: Long,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Long {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Long
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -1,6 +1,9 @@
package com.topjohnwu.magisk.data.preference package com.topjohnwu.magisk.data.preference
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.edit
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
abstract class Property { abstract class Property {
@@ -19,3 +22,147 @@ abstract class Property {
fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value
} }
class BooleanProperty(
private val name: String,
private val default: Boolean,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Boolean {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Boolean
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class FloatProperty(
private val name: String,
private val default: Float,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Float {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Float
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class IntProperty(
private val name: String,
private val default: Int,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Int {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Int
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class LongProperty(
private val name: String,
private val default: Long,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Long {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Long
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class StringProperty(
private val name: String,
private val default: String,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, String> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): String {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: String
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}
class StringSetProperty(
private val name: String,
private val default: Set<String>,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Set<String> {
val prefName = name.ifBlank { property.name }
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Set<String>
) {
val prefName = name.ifBlank { property.name }
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.ktx.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class StringProperty(
private val name: String,
private val default: String,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, String> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): String {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: String
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.ktx.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class StringSetProperty(
private val name: String,
private val default: Set<String>,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Set<String> {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Set<String>
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -35,14 +35,12 @@ class DBSettingsValue(
private val default: Int private val default: Int
) : ReadWriteProperty<DBConfig, Int> { ) : ReadWriteProperty<DBConfig, Int> {
private var value: Int? = null var value: Int? = null
@Synchronized @Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int { override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null) if (value == null)
value = runBlocking { value = runBlocking { thisRef.settingsDB.fetch(name, default) }
thisRef.settingsDB.fetch(name, default)
}
return value as Int return value as Int
} }
@@ -56,7 +54,7 @@ class DBSettingsValue(
} }
} }
class DBBoolSettings( open class DBBoolSettings(
name: String, name: String,
default: Boolean default: Boolean
) : ReadWriteProperty<DBConfig, Boolean> { ) : ReadWriteProperty<DBConfig, Boolean> {
@@ -70,6 +68,17 @@ class DBBoolSettings(
base.setValue(thisRef, property, if (value) 1 else 0) base.setValue(thisRef, property, if (value) 1 else 0)
} }
class DBBoolSettingsNoWrite(
name: String,
default: Boolean
) : DBBoolSettings(name, default) {
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) {
synchronized(base) {
base.value = if (value) 1 else 0
}
}
}
class DBStringsValue( class DBStringsValue(
private val name: String, private val name: String,
private val default: String, private val default: String,

View File

@@ -1,6 +1,7 @@
package com.topjohnwu.magisk.data.repository package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.su.SuLog import com.topjohnwu.magisk.core.model.su.SuLog
import com.topjohnwu.magisk.data.database.SuLogDao import com.topjohnwu.magisk.data.database.SuLogDao
import com.topjohnwu.magisk.ktx.await import com.topjohnwu.magisk.ktx.await
@@ -27,7 +28,11 @@ class LogRepository(
} }
} }
} }
Shell.su("cat ${Const.MAGISK_LOG}").to(list).await() if (Info.env.isActive) {
Shell.su("cat ${Const.MAGISK_LOG} || logcat -d -s Magisk").to(list).await()
} else {
Shell.sh("logcat -d").to(list).await()
}
return list.buf.toString() return list.buf.toString()
} }

View File

@@ -6,9 +6,10 @@ import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.* import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubPageServices
import com.topjohnwu.magisk.data.network.RawServices
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
@@ -16,7 +17,6 @@ import java.io.IOException
class NetworkService( class NetworkService(
private val pages: GithubPageServices, private val pages: GithubPageServices,
private val raw: RawServices, private val raw: RawServices,
private val jsd: JSDelivrServices,
private val api: GithubApiServices private val api: GithubApiServices
) { ) {
suspend fun fetchUpdate() = safe { suspend fun fetchUpdate() = safe {
@@ -27,7 +27,7 @@ class NetworkService(
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl) CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException() else -> throw IllegalArgumentException()
} }
if (info.magisk.versionCode < Info.env.magiskVersionCode && if (info.magisk.versionCode < Info.env.versionCode &&
Config.updateChannel == DEFAULT_CHANNEL Config.updateChannel == DEFAULT_CHANNEL
) { ) {
Config.updateChannel = BETA_CHANNEL Config.updateChannel = BETA_CHANNEL
@@ -59,20 +59,8 @@ class NetworkService(
} }
} }
// Modules related
suspend fun fetchRepoInfo(url: String = Const.Url.OFFICIAL_REPO) = safe {
raw.fetchRepoInfo(url)
}
// Fetch files // Fetch files
suspend fun fetchSafetynet() = wrap { jsd.fetchSafetynet() }
suspend fun fetchBootctl() = wrap { jsd.fetchBootctl() }
suspend fun fetchInstaller() = wrap {
val sha = fetchMainVersion()
jsd.fetchInstaller(sha)
}
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) } suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) } suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
suspend fun fetchModuleJson(url: String) = wrap { raw.fetchModuleJson(url) }
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
} }

View File

@@ -1,57 +0,0 @@
package com.topjohnwu.magisk.databinding
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.InsetDrawable
import androidx.databinding.BindingAdapter
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.ktx.startEndToLeftRight
import com.topjohnwu.magisk.ktx.toPx
import com.topjohnwu.magisk.utils.KItemDecoration
import kotlin.math.roundToInt
@BindingAdapter(
"dividerColor",
"dividerHorizontal",
"dividerSize",
"dividerAfterLast",
"dividerMarginStart",
"dividerMarginEnd",
"dividerMarginTop",
"dividerMarginBottom",
requireAll = false
)
fun setDivider(
view: RecyclerView,
color: Int,
horizontal: Boolean,
_size: Float,
_afterLast: Boolean?,
marginStartF: Float,
marginEndF: Float,
marginTopF: Float,
marginBottomF: Float
) {
val orientation = if (horizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
val size = if (_size > 0) _size.roundToInt() else 1.toPx()
val (width, height) = if (horizontal) size to 1 else 1 to size
val afterLast = _afterLast ?: true
val marginStart = marginStartF.roundToInt()
val marginEnd = marginEndF.roundToInt()
val marginTop = marginTopF.roundToInt()
val marginBottom = marginBottomF.roundToInt()
val (marginLeft, marginRight) = view.context.startEndToLeftRight(marginStart, marginEnd)
val drawable = GradientDrawable().apply {
setSize(width, height)
shape = GradientDrawable.RECTANGLE
setColor(color)
}.let {
InsetDrawable(it, marginLeft, marginTop, marginRight, marginBottom)
}
val decoration = KItemDecoration(view.context, orientation)
.setDeco(drawable)
.apply { showAfterLast = afterLast }
view.addItemDecoration(decoration)
}

View File

@@ -4,6 +4,7 @@ import android.animation.ValueAnimator
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Paint import android.graphics.Paint
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.text.Spanned
import android.util.TypedValue import android.util.TypedValue
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -29,11 +30,8 @@ import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.di.ServiceLocator import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.ktx.coroutineScope
import com.topjohnwu.magisk.ktx.replaceRandomWithSpecial
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
import com.topjohnwu.widget.IndeterminateCheckBox import com.topjohnwu.widget.IndeterminateCheckBox
import kotlinx.coroutines.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@BindingAdapter("gone") @BindingAdapter("gone")
@@ -57,10 +55,8 @@ fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
} }
@BindingAdapter("markdownText") @BindingAdapter("markdownText")
fun setMarkdownText(tv: TextView, text: CharSequence) { fun setMarkdownText(tv: TextView, markdown: Spanned) {
tv.coroutineScope.launch(Dispatchers.IO) { ServiceLocator.markwon.setParsedMarkdown(tv, markdown)
ServiceLocator.markwon.setMarkdown(tv, text.toString())
}
} }
@BindingAdapter("onNavigationClick") @BindingAdapter("onNavigationClick")
@@ -78,22 +74,6 @@ fun setImageResource(view: ImageView, drawable: Drawable) {
view.setImageDrawable(drawable) view.setImageDrawable(drawable)
} }
@BindingAdapter("movieBehavior", "movieBehaviorText")
fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
(view.tag as? Job)?.cancel()
view.tag = null
if (isMovieBehavior) {
view.tag = GlobalScope.launch(Dispatchers.Main.immediate) {
while (true) {
delay(150)
view.text = text.replaceRandomWithSpecial()
}
}
} else {
view.text = text
}
}
@BindingAdapter("onTouch") @BindingAdapter("onTouch")
fun setOnTouchListener(view: View, listener: View.OnTouchListener) { fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
view.setOnTouchListener(listener) view.setOnTouchListener(listener)

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.databinding
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.databinding.ListChangeRegistry import androidx.databinding.ListChangeRegistry
@@ -13,11 +13,10 @@ import kotlin.collections.ArrayList
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise. * @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
*/ */
open class DiffObservableList<T>( open class DiffObservableList<T>(
private val callback: Callback<T>, private val callback: Callback<T>,
private val detectMoves: Boolean = true private val detectMoves: Boolean = true
) : AbstractList<T>(), ObservableList<T> { ) : AbstractList<T>(), ObservableList<T> {
private val LIST_LOCK = Object()
protected var list: MutableList<T> = ArrayList() protected var list: MutableList<T> = ArrayList()
private val listeners = ListChangeRegistry() private val listeners = ListChangeRegistry()
protected val listCallback = ObservableListUpdateCallback() protected val listCallback = ObservableListUpdateCallback()
@@ -32,27 +31,25 @@ open class DiffObservableList<T>(
* list into the given one. * list into the given one.
*/ */
fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult { fun calculateDiff(newItems: List<T>): DiffUtil.DiffResult {
val frozenList = synchronized(LIST_LOCK) { val frozenList = ArrayList(list)
ArrayList(list)
}
return doCalculateDiff(frozenList, newItems) return doCalculateDiff(frozenList, newItems)
} }
protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>?): DiffUtil.DiffResult { protected fun doCalculateDiff(oldItems: List<T>, newItems: List<T>): DiffUtil.DiffResult {
return DiffUtil.calculateDiff(object : DiffUtil.Callback() { return DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize() = oldItems.size override fun getOldListSize() = oldItems.size
override fun getNewListSize() = newItems?.size ?: 0 override fun getNewListSize() = newItems.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems!![newItemPosition] val newItem = newItems[newItemPosition]
return callback.areItemsTheSame(oldItem, newItem) return callback.areItemsTheSame(oldItem, newItem)
} }
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems[oldItemPosition] val oldItem = oldItems[oldItemPosition]
val newItem = newItems!![newItemPosition] val newItem = newItems[newItemPosition]
return callback.areContentsTheSame(oldItem, newItem) return callback.areContentsTheSame(oldItem, newItem)
} }
}, detectMoves) }, detectMoves)
@@ -67,9 +64,7 @@ open class DiffObservableList<T>(
*/ */
@MainThread @MainThread
fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) { fun update(newItems: List<T>, diffResult: DiffUtil.DiffResult) {
synchronized(LIST_LOCK) { list = newItems.toMutableList()
list = newItems.toMutableList()
}
diffResult.dispatchUpdatesTo(listCallback) diffResult.dispatchUpdatesTo(listCallback)
} }
@@ -97,29 +92,14 @@ open class DiffObservableList<T>(
listeners.remove(listener) listeners.remove(listener)
} }
override fun get(index: Int): T { override fun get(index: Int) = list[index]
return list[index]
}
override fun add(element: T): Boolean {
list.add(element)
notifyAdd(size - 1, 1)
return true
}
override fun add(index: Int, element: T) { override fun add(index: Int, element: T) {
list.add(index, element) list.add(index, element)
notifyAdd(index, 1) notifyAdd(index, 1)
} }
override fun addAll(elements: Collection<T>): Boolean { override fun addAll(elements: Collection<T>) = addAll(size, elements)
val oldSize = size
val added = list.addAll(elements)
if (added) {
notifyAdd(oldSize, size - oldSize)
}
return added
}
override fun addAll(index: Int, elements: Collection<T>): Boolean { override fun addAll(index: Int, elements: Collection<T>): Boolean {
val added = list.addAll(index, elements) val added = list.addAll(index, elements)
@@ -153,14 +133,6 @@ open class DiffObservableList<T>(
return element return element
} }
fun removeLast(): T? {
if (size > 0) {
val index = size - 1
return removeAt(index)
}
return null
}
override fun set(index: Int, element: T): T { override fun set(index: Int, element: T): T {
val old = list.set(index, element) val old = list.set(index, element)
listeners.notifyChanged(this, index, 1) listeners.notifyChanged(this, index, 1)
@@ -228,6 +200,5 @@ open class DiffObservableList<T>(
modCount += 1 modCount += 1
listeners.notifyRemoved(this@DiffObservableList, position, count) listeners.notifyRemoved(this@DiffObservableList, position, count)
} }
} }
} }

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.databinding
import android.os.Handler import android.os.Handler
import android.os.HandlerThread import android.os.HandlerThread
@@ -82,4 +82,4 @@ class FilterableDiffObservableList<T>(
override val size: Int override val size: Int
get() = sublist.size get() = sublist.size
} }

View File

@@ -0,0 +1,35 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.ViewDataBinding
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.OnItemBind
fun <T : AnyDiffRvItem> diffListOf() =
DiffObservableList(DiffRvItem.callback<T>())
fun <T : AnyDiffRvItem> diffListOf(newItems: List<T>) =
DiffObservableList(DiffRvItem.callback<T>()).also { it.update(newItems) }
fun <T : AnyDiffRvItem> filterableListOf() =
FilterableDiffObservableList(DiffRvItem.callback<T>())
fun <T : RvItem> adapterOf() = object : BindingRecyclerViewAdapter<T>() {
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,
layoutRes: Int,
position: Int,
item: T
) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
item.onBindingBound(binding)
}
}
inline fun <T : RvItem> itemBindingOf(
crossinline body: (ItemBinding<*>) -> Unit = {}
) = OnItemBind<T> { itemBinding, _, item ->
item.bind(itemBinding)
body(itemBinding)
}

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.databinding
import androidx.databinding.Observable import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry import androidx.databinding.PropertyChangeRegistry

View File

@@ -5,8 +5,6 @@ import androidx.databinding.PropertyChangeRegistry
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.ObservableHost
import me.tatarka.bindingcollectionadapter2.ItemBinding import me.tatarka.bindingcollectionadapter2.ItemBinding
abstract class RvItem { abstract class RvItem {
@@ -26,38 +24,57 @@ abstract class RvItem {
open fun onBindingBound(binding: ViewDataBinding) {} open fun onBindingBound(binding: ViewDataBinding) {}
} }
abstract class ComparableRvItem<in T> : RvItem() { interface RvContainer<E> {
val item: E
}
// Use Any.equals by default interface ComparableRv<T> : Comparable<T> {
open fun itemSameAs(other: T) = this == other
// Use compareTo if this is Comparable or assume not same
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun comparableEqual(o: Any?) =
o != null && o::class == this::class && compareTo(o as T) == 0
}
abstract class DiffRvItem<T> : RvItem() {
// Defer to contentSameAs by default
open fun itemSameAs(other: T) = true
open fun contentSameAs(other: T) = open fun contentSameAs(other: T) =
(this as? Comparable<T>)?.run { compareTo(other) == 0 } ?: false when (this) {
is RvContainer<*> -> item == (other as RvContainer<*>).item
@Suppress("UNCHECKED_CAST") is ComparableRv<*> -> comparableEqual(other)
open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T) else -> this == other
}
@Suppress("UNCHECKED_CAST")
open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T)
companion object { companion object {
val callback = object : DiffObservableList.Callback<ComparableRvItem<*>> { private val callback = object : DiffObservableList.Callback<DiffRvItem<Any>> {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: ComparableRvItem<*>, oldItem: DiffRvItem<Any>,
newItem: ComparableRvItem<*> newItem: DiffRvItem<Any>
) = oldItem.genericItemSameAs(newItem) ): Boolean {
return oldItem::class == newItem::class && oldItem.itemSameAs(newItem)
}
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: ComparableRvItem<*>, oldItem: DiffRvItem<Any>,
newItem: ComparableRvItem<*> newItem: DiffRvItem<Any>
) = oldItem.genericContentSameAs(newItem) ): Boolean {
return oldItem.contentSameAs(newItem)
}
} }
@Suppress("UNCHECKED_CAST")
fun <T : AnyDiffRvItem> callback() = callback as DiffObservableList.Callback<T>
} }
} }
abstract class ObservableItem<T> : ComparableRvItem<T>(), ObservableHost { typealias AnyDiffRvItem = DiffRvItem<*>
abstract class ObservableDiffRvItem<T> : DiffRvItem<T>(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null
}
abstract class ObservableRvItem : RvItem(), ObservableHost {
override var callbacks: PropertyChangeRegistry? = null override var callbacks: PropertyChangeRegistry? = null
} }

View File

@@ -6,10 +6,9 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.ProviderInstaller import com.topjohnwu.magisk.ProviderInstaller
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.ktx.precomputedText import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.utils.MarkwonImagePlugin import okhttp3.Cache
import io.noties.markwon.Markwon import okhttp3.ConnectionSpec
import io.noties.markwon.html.HtmlPlugin
import okhttp3.Dns import okhttp3.Dns
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -18,6 +17,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory import retrofit2.converter.scalars.ScalarsConverterFactory
import java.io.File
import java.net.InetAddress import java.net.InetAddress
import java.net.UnknownHostException import java.net.UnknownHostException
@@ -31,7 +31,6 @@ private class DnsResolver(client: OkHttpClient) : Dns {
InetAddress.getByName("162.159.46.1"), InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"), InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"), InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"), InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"), InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"), InetAddress.getByName("2606:4700:4700::0064"),
@@ -51,20 +50,31 @@ private class DnsResolver(client: OkHttpClient) : Dns {
} }
} }
@Suppress("DEPRECATION")
fun createOkHttpClient(context: Context): OkHttpClient { fun createOkHttpClient(context: Context): OkHttpClient {
val builder = OkHttpClient.Builder() val appCache = Cache(File(context.cacheDir, "okhttp"), 10 * 1024 * 1024)
val builder = OkHttpClient.Builder().cache(appCache)
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
builder.addInterceptor(HttpLoggingInterceptor().apply { builder.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC level = HttpLoggingInterceptor.Level.BASIC
}) })
} else {
builder.connectionSpecs(listOf(ConnectionSpec.MODERN_TLS))
}
builder.dns(DnsResolver(builder.build()))
builder.addInterceptor { chain ->
val request = chain.request().newBuilder()
request.header("User-Agent", "Magisk/${BuildConfig.VERSION_CODE}")
request.header("Accept-Language", currentLocale.toLanguageTag())
chain.proceed(request.build())
} }
if (!ProviderInstaller.install(context)) { if (!ProviderInstaller.install(context)) {
Info.hasGMS = false Info.hasGMS = false
} }
builder.dns(DnsResolver(builder.build()))
return builder.build() return builder.build()
} }
@@ -87,14 +97,3 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
.build() .build()
.create(T::class.java) .create(T::class.java)
} }
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
return Markwon.builder(context)
.textSetter { textView, spanned, _, onComplete ->
textView.tag = onComplete
textView.precomputedText = spanned
}
.usePlugin(HtmlPlugin.create())
.usePlugin(MarkwonImagePlugin(okHttpClient))
.build()
}

View File

@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.di
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.text.method.LinkMovementMethod
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.ViewModelStoreOwner
@@ -10,8 +11,6 @@ import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.magiskdb.PolicyDao import com.topjohnwu.magisk.core.magiskdb.PolicyDao
import com.topjohnwu.magisk.core.magiskdb.SettingsDao import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao import com.topjohnwu.magisk.core.magiskdb.StringDao
import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoDatabase
import com.topjohnwu.magisk.data.database.SuLogDatabase import com.topjohnwu.magisk.data.database.SuLogDatabase
import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.data.repository.NetworkService import com.topjohnwu.magisk.data.repository.NetworkService
@@ -19,10 +18,10 @@ import com.topjohnwu.magisk.ktx.deviceProtectedContext
import com.topjohnwu.magisk.ui.home.HomeViewModel import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.ui.install.InstallViewModel import com.topjohnwu.magisk.ui.install.InstallViewModel
import com.topjohnwu.magisk.ui.log.LogViewModel import com.topjohnwu.magisk.ui.log.LogViewModel
import com.topjohnwu.magisk.ui.module.ModuleViewModel
import com.topjohnwu.magisk.ui.settings.SettingsViewModel
import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel import com.topjohnwu.magisk.ui.superuser.SuperuserViewModel
import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel import com.topjohnwu.magisk.ui.surequest.SuRequestViewModel
import io.noties.markwon.Markwon
import io.noties.markwon.utils.NoCopySpannableFactory
val AppContext: Context inline get() = ServiceLocator.context val AppContext: Context inline get() = ServiceLocator.context
@@ -37,36 +36,31 @@ object ServiceLocator {
val policyDB = PolicyDao() val policyDB = PolicyDao()
val settingsDB = SettingsDao() val settingsDB = SettingsDao()
val stringDB = StringDao() val stringDB = StringDao()
val repoDB by lazy { createRepoDatabase(context).repoDao() }
val sulogDB by lazy { createSuLogDatabase(deContext).suLogDao() } val sulogDB by lazy { createSuLogDatabase(deContext).suLogDao() }
val repoUpdater by lazy { RepoUpdater(networkService, repoDB) }
val logRepo by lazy { LogRepository(sulogDB) } val logRepo by lazy { LogRepository(sulogDB) }
// Networking // Networking
val okhttp by lazy { createOkHttpClient(context) } val okhttp by lazy { createOkHttpClient(context) }
val retrofit by lazy { createRetrofit(okhttp) } val retrofit by lazy { createRetrofit(okhttp) }
val markwon by lazy { createMarkwon(context, okhttp) } val markwon by lazy { createMarkwon(context) }
val networkService by lazy { val networkService by lazy {
NetworkService( NetworkService(
createApiService(retrofit, Const.Url.GITHUB_PAGE_URL), createApiService(retrofit, Const.Url.GITHUB_PAGE_URL),
createApiService(retrofit, Const.Url.GITHUB_RAW_URL), createApiService(retrofit, Const.Url.GITHUB_RAW_URL),
createApiService(retrofit, Const.Url.JS_DELIVR_URL),
createApiService(retrofit, Const.Url.GITHUB_API_URL) createApiService(retrofit, Const.Url.GITHUB_API_URL)
) )
} }
object VMFactory : ViewModelProvider.Factory { object VMFactory : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(clz: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return when (clz) { return when (modelClass) {
HomeViewModel::class.java -> HomeViewModel(networkService) HomeViewModel::class.java -> HomeViewModel(networkService)
LogViewModel::class.java -> LogViewModel(logRepo) LogViewModel::class.java -> LogViewModel(logRepo)
ModuleViewModel::class.java -> ModuleViewModel(repoDB, repoUpdater)
SettingsViewModel::class.java -> SettingsViewModel(repoDB)
SuperuserViewModel::class.java -> SuperuserViewModel(policyDB) SuperuserViewModel::class.java -> SuperuserViewModel(policyDB)
InstallViewModel::class.java -> InstallViewModel(networkService) InstallViewModel::class.java -> InstallViewModel(networkService)
SuRequestViewModel::class.java -> SuRequestViewModel(policyDB, timeoutPrefs) SuRequestViewModel::class.java -> SuRequestViewModel(policyDB, timeoutPrefs)
else -> clz.newInstance() else -> modelClass.newInstance()
} as T } as T
} }
} }
@@ -74,15 +68,20 @@ object ServiceLocator {
inline fun <reified VM : ViewModel> ViewModelStoreOwner.viewModel() = inline fun <reified VM : ViewModel> ViewModelStoreOwner.viewModel() =
lazy(LazyThreadSafetyMode.NONE) { lazy(LazyThreadSafetyMode.NONE) {
ViewModelProvider(this, ServiceLocator.VMFactory).get(VM::class.java) ViewModelProvider(this, ServiceLocator.VMFactory)[VM::class.java]
} }
private fun createRepoDatabase(context: Context) =
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
.fallbackToDestructiveMigration()
.build()
private fun createSuLogDatabase(context: Context) = private fun createSuLogDatabase(context: Context) =
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db") Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
.fallbackToDestructiveMigration() .fallbackToDestructiveMigration()
.build() .build()
private fun createMarkwon(context: Context) =
Markwon.builder(context).textSetter { textView, spanned, bufferType, onComplete ->
textView.apply {
movementMethod = LinkMovementMethod.getInstance()
setSpannableFactory(NoCopySpannableFactory.getInstance())
setText(spanned, bufferType)
onComplete.run()
}
}.build()

View File

@@ -1,33 +0,0 @@
package com.topjohnwu.magisk.events
import android.content.Context
import android.content.res.Resources
import android.util.TypedValue
import androidx.annotation.AttrRes
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.net.toUri
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ContextExecutor
import com.topjohnwu.magisk.arch.ViewEvent
data class OpenInappLinkEvent(
private val link: String
) : ViewEvent(), ContextExecutor {
// todo find app that can open the link and as a fallback open custom tabs! it shouldn't be the default
override fun invoke(context: Context) = CustomTabsIntent.Builder()
.setShowTitle(true)
.setToolbarColor(context.themedColor(R.attr.colorSurface))
.enableUrlBarHiding()
.build()
.launchUrl(context, link.toUri())
private fun Context.themedColor(@AttrRes attribute: Int) = theme
.resolveAttribute(attribute).data
private fun Resources.Theme.resolveAttribute(
@AttrRes attribute: Int,
resolveRefs: Boolean = true
) = TypedValue().also { resolveAttribute(attribute, it, resolveRefs) }
}

View File

@@ -4,7 +4,7 @@ import android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.magisk.arch.ActivityExecutor import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.BaseUIActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.utils.TextHolder import com.topjohnwu.magisk.utils.TextHolder
import com.topjohnwu.magisk.utils.asText import com.topjohnwu.magisk.utils.asText
@@ -30,13 +30,14 @@ class SnackbarEvent constructor(
private fun snackbar( private fun snackbar(
view: View, view: View,
anchor: View?,
message: String, message: String,
length: Int, length: Int,
builder: Snackbar.() -> Unit builder: Snackbar.() -> Unit
) = Snackbar.make(view, message, length).apply(builder).show() ) = Snackbar.make(view, message, length).setAnchorView(anchor).apply(builder).show()
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
snackbar(activity.snackbarView, snackbar(activity.snackbarView, activity.snackbarAnchorView,
msg.getText(activity.resources).toString(), msg.getText(activity.resources).toString(),
length, builder) length, builder)
} }

View File

@@ -1,9 +1,8 @@
package com.topjohnwu.magisk.events package com.topjohnwu.magisk.events
import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.net.Uri
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
@@ -11,76 +10,50 @@ import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.* import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.ActivityResultCallback
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.events.dialog.MarkDownDialog
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) = action(activity)
}
class OpenReadmeEvent(private val item: OnlineModule) : MarkDownDialog() {
override suspend fun getMarkdownText() = item.notes()
override fun build(dialog: MagiskDialog) {
super.build(dialog)
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}.cancellable(true)
}
}
class PermissionEvent( class PermissionEvent(
private val permission: String, private val permission: String,
private val callback: (Boolean) -> Unit private val callback: (Boolean) -> Unit
) : ViewEvent(), ActivityExecutor { ) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) = override fun invoke(activity: UIActivity<*>) =
activity.withPermission(permission) { activity.withPermission(permission, callback)
onSuccess {
callback(true)
}
onFailure {
callback(false)
}
}
} }
class BackPressEvent : ViewEvent(), ActivityExecutor { class BackPressEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
activity.onBackPressed() activity.onBackPressed()
} }
} }
class DieEvent : ViewEvent(), ActivityExecutor { class DieEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
activity.finish() activity.finish()
} }
} }
class ShowUIEvent(private val delegate: View.AccessibilityDelegate?) class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
: ViewEvent(), ActivityExecutor { : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
activity.setContentView() activity.setContentView()
activity.setAccessibilityDelegate(delegate) activity.setAccessibilityDelegate(delegate)
} }
} }
class RecreateEvent : ViewEvent(), ActivityExecutor { class RecreateEvent : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
activity.recreate() activity.recreate()
} }
} }
class MagiskInstallFileEvent(private val callback: ActivityResultCallback) class MagiskInstallFileEvent(
: ViewEvent(), ActivityExecutor { private val callback: (Uri) -> Unit
override fun invoke(activity: BaseUIActivity<*, *>) { ) : ViewEvent(), ActivityExecutor {
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("*/*") override fun invoke(activity: UIActivity<*>) {
try { try {
activity.startActivityForResult(intent, callback) activity.getContent("*/*", callback)
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG) Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT) Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
@@ -89,10 +62,12 @@ class MagiskInstallFileEvent(private val callback: ActivityResultCallback)
} }
class NavigationEvent( class NavigationEvent(
private val directions: NavDirections private val directions: NavDirections,
private val pop: Boolean
) : ViewEvent(), ActivityExecutor { ) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
(activity as? BaseUIActivity<*, *>)?.apply { (activity as? NavigationActivity<*>)?.apply {
if (pop) navigation?.popBackStack()
directions.navigate() directions.navigate()
} }
} }
@@ -105,16 +80,11 @@ class AddHomeIconEvent : ViewEvent(), ContextExecutor {
} }
class SelectModuleEvent : ViewEvent(), FragmentExecutor { class SelectModuleEvent : ViewEvent(), FragmentExecutor {
override fun invoke(fragment: BaseUIFragment<*, *>) { override fun invoke(fragment: BaseFragment<*>) {
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("application/zip")
try { try {
fragment.apply { fragment.apply {
activity.startActivityForResult(intent) { code, intent -> activity?.getContent("application/zip") {
if (code == Activity.RESULT_OK && intent != null) { MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
intent.data?.also {
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
}
}
} }
} }
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.arch.ActivityExecutor import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.BaseUIActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.utils.BiometricHelper import com.topjohnwu.magisk.core.utils.BiometricHelper
@@ -16,7 +16,7 @@ class BiometricEvent(
builder(Builder()) builder(Builder())
} }
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: UIActivity<*>) {
BiometricHelper.authenticate( BiometricHelper.authenticate(
activity, activity,
onError = listenerOnFailure, onError = listenerOnFailure,

View File

@@ -1,8 +1,6 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
@@ -11,31 +9,25 @@ import com.topjohnwu.magisk.view.MagiskDialog
class DarkThemeDialog : DialogEvent() { class DarkThemeDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
val activity = dialog.context.unwrap() val activity = dialog.ownerActivity!!
dialog.applyTitle(R.string.settings_dark_mode_title) dialog.apply {
.applyMessage(R.string.settings_dark_mode_message) setTitle(R.string.settings_dark_mode_title)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { setMessage(R.string.settings_dark_mode_message)
titleRes = R.string.settings_dark_mode_light setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.settings_dark_mode_light
icon = R.drawable.ic_day icon = R.drawable.ic_day
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_NO, activity) } onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_NO, activity) }
} }
.applyButton(MagiskDialog.ButtonType.NEUTRAL) { setButton(MagiskDialog.ButtonType.NEUTRAL) {
titleRes = R.string.settings_dark_mode_system text = R.string.settings_dark_mode_system
icon = R.drawable.ic_day_night icon = R.drawable.ic_day_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, activity) } onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM, activity) }
} }
.applyButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.settings_dark_mode_dark text = R.string.settings_dark_mode_dark
icon = R.drawable.ic_night icon = R.drawable.ic_night
onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_YES, activity) } onClick { selectTheme(AppCompatDelegate.MODE_NIGHT_YES, activity) }
} }
}
private fun Context.unwrap(): Activity {
return when(this) {
is Activity -> this
is ContextWrapper -> baseContext.unwrap()
else -> error("Cannot happen")
} }
} }

View File

@@ -1,18 +1,16 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.arch.ActivityExecutor import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.BaseUIActivity import com.topjohnwu.magisk.arch.UIActivity
import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
abstract class DialogEvent : ViewEvent(), ActivityExecutor { abstract class DialogEvent : ViewEvent(), ActivityExecutor {
protected lateinit var dialog: MagiskDialog override fun invoke(activity: UIActivity<*>) {
MagiskDialog(activity)
override fun invoke(activity: BaseUIActivity<*, *>) {
dialog = MagiskDialog(activity)
.apply { setOwnerActivity(activity) } .apply { setOwnerActivity(activity) }
.apply(this::build).reveal() .apply(this::build).show()
} }
abstract fun build(dialog: MagiskDialog) abstract fun build(dialog: MagiskDialog)

View File

@@ -1,38 +1,52 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.ui.home.HomeViewModel
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class EnvFixDialog : DialogEvent() { class EnvFixDialog(private val vm: HomeViewModel) : DialogEvent() {
override fun build(dialog: MagiskDialog) = dialog override fun build(dialog: MagiskDialog) {
.applyTitle(R.string.env_fix_title) dialog.apply {
.applyMessage(R.string.env_fix_msg) setTitle(R.string.env_fix_title)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { setMessage(R.string.env_fix_msg)
titleRes = android.R.string.ok setButton(MagiskDialog.ButtonType.POSITIVE) {
preventDismiss = true text = android.R.string.ok
onClick { doNotDismiss = true
dialog.applyTitle(R.string.setup_title) onClick {
.applyMessage(R.string.setup_msg) dialog.apply {
.resetButtons() setTitle(R.string.setup_title)
.cancellable(false) setMessage(R.string.setup_msg)
(dialog.ownerActivity as BaseActivity).lifecycleScope.launch { resetButtons()
MagiskInstaller.FixEnv { setCancelable(false)
dialog.dismiss() }
}.exec() (dialog.ownerActivity as BaseActivity).lifecycleScope.launch {
MagiskInstaller.FixEnv {
dialog.dismiss()
}.exec()
}
}
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
}
if (Info.env.versionCode != BuildConfig.VERSION_CODE ||
Info.env.versionString != BuildConfig.VERSION_NAME) {
dialog.setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick {
vm.onMagiskPressed()
dialog.dismiss()
} }
} }
} }
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}
.let { }
companion object {
const val DISMISS = "com.topjohnwu.magisk.ENV_DONE"
} }
} }

View File

@@ -25,14 +25,14 @@ class ManagerInstallDialog : MarkDownDialog() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
super.build(dialog) super.build(dialog)
with(dialog) { dialog.apply {
setCancelable(true) setCancelable(true)
applyButton(MagiskDialog.ButtonType.POSITIVE) { setButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.install text = R.string.install
onClick { DownloadService.start(context, Subject.Manager()) } onClick { DownloadService.start(context, Subject.Manager()) }
} }
applyButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel text = android.R.string.cancel
} }
} }
} }

View File

@@ -12,7 +12,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException import java.io.IOException
abstract class MarkDownDialog : DialogEvent() { abstract class MarkDownDialog : DialogEvent() {
@@ -22,18 +22,15 @@ abstract class MarkDownDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
with(dialog) { with(dialog) {
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null) val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
applyView(view) setView(view)
val tv = view.findViewById<TextView>(R.id.md_txt)
(ownerActivity as BaseActivity).lifecycleScope.launch { (ownerActivity as BaseActivity).lifecycleScope.launch {
val tv = view.findViewById<TextView>(R.id.md_txt) try {
withContext(Dispatchers.IO) { val text = withContext(Dispatchers.IO) { getMarkdownText() }
try { ServiceLocator.markwon.setMarkdown(tv, text)
ServiceLocator.markwon.setMarkdown(tv, getMarkdownText()) } catch (e: IOException) {
} catch (e: Exception) { Timber.e(e)
if (e is CancellationException) tv.setText(R.string.download_file_error)
throw e
Timber.e(e)
tv.post { tv.setText(R.string.download_file_error) }
}
} }
} }
} }

View File

@@ -1,42 +1,48 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.OnlineModule import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.di.ServiceLocator
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() { class ModuleInstallDialog(private val item: OnlineModule) : MarkDownDialog() {
private val svc get() = ServiceLocator.networkService
override suspend fun getMarkdownText(): String {
val str = svc.fetchString(item.changelog)
return if (str.length > 1000) str.substring(0, 1000) else str
}
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
with(dialog) { super.build(dialog)
dialog.apply {
fun download(install: Boolean) { fun download(install: Boolean) {
val config = if (install) Action.Flash else Action.Download val action = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, config) val subject = Subject.Module(item, action)
DownloadService.start(context, subject) DownloadService.start(context, subject)
} }
applyTitle(context.getString(R.string.repo_install_title, item.name)) val title = context.getString(R.string.repo_install_title,
.applyMessage(context.getString(R.string.repo_install_msg, item.downloadFilename)) item.name, item.version, item.versionCode)
.cancellable(true)
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.download
icon = R.drawable.ic_download_md2
onClick { download(false) }
}
if (Info.env.isActive) { setTitle(title)
applyButton(MagiskDialog.ButtonType.POSITIVE) { setCancelable(true)
titleRes = R.string.install setButton(MagiskDialog.ButtonType.NEGATIVE) {
icon = R.drawable.ic_install text = R.string.download
onClick { download(true) } onClick { download(false) }
} }
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = R.string.install
onClick { download(true) }
}
setButton(MagiskDialog.ButtonType.NEUTRAL) {
text = android.R.string.cancel
} }
reveal()
} }
} }

View File

@@ -0,0 +1,22 @@
package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.view.MagiskDialog
class RestoreAppDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) {
dialog.apply {
setTitle(R.string.settings_restore_app_title)
setMessage(R.string.restore_app_confirmation)
setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick { HideAPK.restore(dialog.ownerActivity!!) }
}
setButton(MagiskDialog.ButtonType.NEGATIVE) {
text = android.R.string.cancel
}
setCancelable(true)
}
}
}

View File

@@ -6,11 +6,13 @@ import com.topjohnwu.magisk.view.MagiskDialog
class SecondSlotWarningDialog : DialogEvent() { class SecondSlotWarningDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
dialog.applyTitle(android.R.string.dialog_alert_title) dialog.apply {
.applyMessage(R.string.install_inactive_slot_msg) setTitle(android.R.string.dialog_alert_title)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { setMessage(R.string.install_inactive_slot_msg)
titleRes = android.R.string.ok setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
} }
.cancellable(true) setCancelable(true)
}
} }
} }

View File

@@ -10,15 +10,17 @@ class SuperuserRevokeDialog(
private val callbacks = Builder().apply(builder) private val callbacks = Builder().apply(builder)
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
dialog.applyTitle(R.string.su_revoke_title) dialog.apply {
.applyMessage(R.string.su_revoke_msg, callbacks.appName) setTitle(R.string.su_revoke_title)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { setMessage(R.string.su_revoke_msg, callbacks.appName)
titleRes = android.R.string.ok setButton(MagiskDialog.ButtonType.POSITIVE) {
text = android.R.string.ok
onClick { callbacks.listenerOnSuccess() } onClick { callbacks.listenerOnSuccess() }
} }
.applyButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel text = android.R.string.cancel
} }
}
} }
inner class Builder internal constructor() { inner class Builder internal constructor() {

View File

@@ -1,9 +1,10 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Context
import android.widget.Toast import android.widget.Toast
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIActivity import com.topjohnwu.magisk.arch.NavigationActivity
import com.topjohnwu.magisk.ui.flash.FlashFragment import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
@@ -12,22 +13,24 @@ import com.topjohnwu.superuser.Shell
class UninstallDialog : DialogEvent() { class UninstallDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
dialog.applyTitle(R.string.uninstall_magisk_title) dialog.apply {
.applyMessage(R.string.uninstall_magisk_msg) setTitle(R.string.uninstall_magisk_title)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { setMessage(R.string.uninstall_magisk_msg)
titleRes = R.string.restore_img setButton(MagiskDialog.ButtonType.POSITIVE) {
onClick { restore() } text = R.string.restore_img
onClick { restore(dialog.context) }
} }
.applyButton(MagiskDialog.ButtonType.NEGATIVE) { setButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.complete_uninstall text = R.string.complete_uninstall
onClick { completeUninstall() } onClick { completeUninstall(dialog) }
} }
}
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
private fun restore() { private fun restore(context: Context) {
val dialog = ProgressDialog(dialog.context).apply { val dialog = ProgressDialog(context).apply {
setMessage(dialog.context.getString(R.string.restore_img_msg)) setMessage(context.getString(R.string.restore_img_msg))
show() show()
} }
@@ -41,9 +44,9 @@ class UninstallDialog : DialogEvent() {
} }
} }
private fun completeUninstall() { private fun completeUninstall(dialog: MagiskDialog) {
(dialog.ownerActivity as? BaseUIActivity<*, *>) (dialog.ownerActivity as NavigationActivity<*>)
?.navigation?.navigate(FlashFragment.uninstall()) .navigation.navigate(FlashFragment.uninstall())
} }
} }

View File

@@ -1,9 +0,0 @@
package com.topjohnwu.magisk.ktx
import android.content.res.Resources
import kotlin.math.ceil
import kotlin.math.roundToInt
fun Int.toDp(): Int = ceil(this / Resources.getSystem().displayMetrics.density).roundToInt()
fun Int.toPx(): Int = (this * Resources.getSystem().displayMetrics.density).roundToInt()

View File

@@ -1,193 +0,0 @@
@file:Suppress("unused")
package com.topjohnwu.magisk.ktx
import android.graphics.Canvas
import android.graphics.Rect
import android.view.View
import android.widget.EdgeEffect
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
fun RecyclerView.addInvalidateItemDecorationsObserver() {
adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
invalidateItemDecorations()
}
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
invalidateItemDecorations()
}
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
invalidateItemDecorations()
}
})
}
fun RecyclerView.addVerticalPadding(paddingTop: Int = 0, paddingBottom: Int = 0) {
addItemDecoration(VerticalPaddingDecoration(paddingTop, paddingBottom))
}
private class VerticalPaddingDecoration(private val paddingTop: Int = 0, private val paddingBottom: Int = 0) : RecyclerView.ItemDecoration() {
private var allowTop: Boolean = true
private var allowBottom: Boolean = true
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
val adapter = parent.adapter ?: return
val position = parent.getChildAdapterPosition(view)
val count = adapter.itemCount
if (position == 0 && allowTop) {
outRect.top = paddingTop
} else if (position == count - 1 && allowBottom) {
outRect.bottom = paddingBottom
}
}
}
fun RecyclerView.addSimpleItemDecoration(
left: Int = 0,
top: Int = 0,
right: Int = 0,
bottom: Int = 0,
) {
addItemDecoration(SimpleItemDecoration(left, top, right, bottom))
}
private class SimpleItemDecoration(
private val left: Int = 0,
private val top: Int = 0,
private val right: Int = 0,
private val bottom: Int = 0
) : RecyclerView.ItemDecoration() {
private var allowLeft: Boolean = true
private var allowTop: Boolean = true
private var allowRight: Boolean = true
private var allowBottom: Boolean = true
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
if (parent.adapter == null) {
return
}
if (allowLeft) {
outRect.left = left
}
if (allowTop) {
outRect.top = top
}
if (allowRight) {
outRect.right = right
}
if (allowBottom) {
outRect.top = bottom
}
}
}
fun RecyclerView.fixEdgeEffect(overScrollIfContentScrolls: Boolean = true, alwaysClipToPadding: Boolean = true) {
if (overScrollIfContentScrolls) {
val listener = OverScrollIfContentScrollsListener()
addOnLayoutChangeListener(listener)
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, listener)
} else {
val listener = getTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener) as? OverScrollIfContentScrollsListener
if (listener != null) {
removeOnLayoutChangeListener(listener)
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, null)
}
}
edgeEffectFactory = if (alwaysClipToPadding && !clipToPadding) {
AlwaysClipToPaddingEdgeEffectFactory()
} else {
RecyclerView.EdgeEffectFactory()
}
}
private class OverScrollIfContentScrollsListener : View.OnLayoutChangeListener {
private var show = true
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
if (shouldDrawOverScroll(v as RecyclerView) != show) {
show = !show
if (show) {
v.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS)
} else {
v.setOverScrollMode(View.OVER_SCROLL_NEVER)
}
}
}
fun shouldDrawOverScroll(recyclerView: RecyclerView): Boolean {
if (recyclerView.layoutManager == null || recyclerView.adapter == null || recyclerView.adapter!!.itemCount == 0) {
return false
}
if (recyclerView.layoutManager is LinearLayoutManager) {
val itemCount = recyclerView.layoutManager!!.itemCount
val firstPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findFirstCompletelyVisibleItemPosition()
val lastPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition()
return firstPosition != 0 || lastPosition != itemCount - 1
}
return true
}
}
private class AlwaysClipToPaddingEdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
return object : EdgeEffect(view.context) {
private var ensureSize = false
private fun ensureSize() {
if (ensureSize) return
ensureSize = true
when (direction) {
DIRECTION_LEFT -> {
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
view.measuredWidth - view.paddingLeft - view.paddingRight)
}
DIRECTION_TOP -> {
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
view.measuredHeight - view.paddingTop - view.paddingBottom)
}
DIRECTION_RIGHT -> {
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
view.measuredWidth - view.paddingLeft - view.paddingRight)
}
DIRECTION_BOTTOM -> {
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
view.measuredHeight - view.paddingTop - view.paddingBottom)
}
}
}
override fun draw(c: Canvas): Boolean {
ensureSize()
val restore = c.save()
when (direction) {
DIRECTION_LEFT -> {
c.translate(view.paddingBottom.toFloat(), 0f)
}
DIRECTION_TOP -> {
c.translate(view.paddingLeft.toFloat(), view.paddingTop.toFloat())
}
DIRECTION_RIGHT -> {
c.translate(-view.paddingTop.toFloat(), 0f)
}
DIRECTION_BOTTOM -> {
c.translate(view.paddingRight.toFloat(), view.paddingBottom.toFloat())
}
}
val res = super.draw(c)
c.restoreToCount(restore)
return res
}
}
}
}

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