Compare commits

...

386 Commits

Author SHA1 Message Date
topjohnwu
483dbcdc40 Release v22.0 2021-02-23 04:09:26 -08:00
topjohnwu
a1096b5bf0 Do not run pm install on main thread 2021-02-23 04:09:13 -08:00
Chris Renshaw
5ac0e64edb Update guides.md for system_ext 2021-02-23 03:27:36 -08:00
Lishoo
60b2624607 Update polish translations
Add missing strings
2021-02-23 03:26:47 -08:00
topjohnwu
d2e2847b03 Fix stub 2021-02-23 03:24:51 -08:00
topjohnwu
b9669f54f7 Update docs 2021-02-23 03:06:00 -08:00
topjohnwu
8c7bd77d33 Do not wrap twice 2021-02-23 01:49:15 -08:00
Shaka Huang
ba1ce16b8b Fix error in pure 64-bit environment
In Android S preview, there’s no 32-bit libraries in x86_64 system image for emulator.

Signed-off-by: Shaka Huang <shakalaca@gmail.com>
2021-02-22 03:28:54 -08:00
topjohnwu
68090943f4 Several changes
- Change error message strings
- Move non-root stub error to SplashActivity
- Skip shell init in non-root stub
2021-02-22 03:28:19 -08:00
vvb2060
a4fb1297b0 Fix crash in pure 64-bit devices 2021-02-22 03:08:51 -08:00
vvb2060
860a05abf2 Simplify UpdateChannel 2021-02-22 03:08:51 -08:00
vvb2060
8bb2f356c0 Allow offline restore app 2021-02-22 03:08:51 -08:00
vvb2060
4950020635 Prevent dot in the first position again 2021-02-22 03:08:51 -08:00
vvb2060
0a6140c6eb Try install with root first 2021-02-22 03:08:51 -08:00
vvb2060
bba2ac8817 Add unsupport env check 2021-02-22 03:08:51 -08:00
topjohnwu
331b1f542f Use standard Android APIs for install and launch 2021-02-20 20:12:35 -08:00
topjohnwu
ccb55205e6 Fix pre 21 support 2021-02-20 03:38:39 -08:00
topjohnwu
9cc91b30b3 Fix #3871 2021-02-20 02:49:43 -08:00
grmasa
e836caf31e Update Greek translation 2021-02-20 01:51:39 -08:00
Lishoo
beaa1e5be2 Add missing strings and small updates. 2021-02-20 01:51:02 -08:00
Lishoo
ea545bae26 Update polish translations 2021-02-20 01:50:44 -08:00
topjohnwu
1c9ec2df45 Publish new canary build 2021-02-14 13:43:24 -08:00
vvb2060
b76c80e2ce Fix apex path 2021-02-14 13:37:38 -08:00
topjohnwu
236990f4a3 Fix stub app crashing 2021-02-14 13:37:13 -08:00
topjohnwu
1ed32df20d Publish new canary build 2021-02-13 17:26:53 -08:00
topjohnwu
8476eb9f4b Avoid patching vendor_boot.img 2021-02-13 17:15:04 -08:00
JoanVC100
735af7843b Add new ca-strings 2021-02-13 17:09:46 -08:00
MC Naveen
ded73e958b Added Tamil Translation 2021-02-13 17:09:28 -08:00
Ooggle
6dcb84d4f4 French translation of newest commit 2021-02-13 17:08:53 -08:00
topjohnwu
501bc9f438 Restore init from backup rather than symlink
Because of course Samsung don't follow AOSP norms.
I mean, why would they?
2021-02-13 16:43:06 -08:00
topjohnwu
f88e812b63 Move behavior to XML 2021-02-13 15:26:32 -08:00
Tornike Khintibidze
be6386c410 Updated Georgian translation 2021-02-12 03:59:35 -08:00
Didgeridoohan
2af4fd17c4 Minor fixes and changes to Swedish transaltions 2021-02-12 03:59:07 -08:00
Mikael Bjurström
f870418bd0 Update Swedish translation 2021-02-12 00:07:40 -08:00
vvb2060
00659e4795 Hide OTA option on virtual A/B devices 2021-02-12 00:07:15 -08:00
Jose Manuel Estrada-Nora Muñoz
cdda10207e Spanish strings 2021-02-11 23:32:24 -08:00
Ilya Kushnir
701700279f Update RU strings 2021-02-11 23:32:04 -08:00
alex26052005
a9d804724a Update strings.xml
Updated German language
2021-02-11 23:31:04 -08:00
DanGLES3
e033a9ab47 Update Portugues Brazilian translation 2021-02-11 23:30:35 -08:00
kubalav
059e5fb8aa Update Slovak translation 2021-02-11 23:28:49 -08:00
vvb2060
a78f255928 Update zh-rCN translation 2021-02-11 23:25:24 -08:00
AndroPlus
1d10e69288 Update Japanese translation 2021-02-11 23:23:36 -08:00
topjohnwu
63590d379c Update hide icon strategy 2021-02-11 22:38:41 -08:00
topjohnwu
5f63e88984 Hide icons when things don't fit 2021-02-11 05:08:40 -08:00
topjohnwu
75584e2b19 App string resources overhaul 2021-02-11 02:34:27 -08:00
vvb2060
1426ee2ebd Fix Android build version sdk in script 2021-02-10 22:22:50 -08:00
topjohnwu
b6643b7bfc Publish new canary build 2021-02-07 21:39:00 -08:00
Hen Ry
721dfdf553 Added translation of new strings 2021-02-07 17:42:33 -08:00
topjohnwu
2963747d14 Fix LZ4_LG format decompression
Fix #3802, fix #3722, fix #3770, fix #3635, fix #3787, close #3812
2021-02-07 17:40:59 -08:00
topjohnwu
e7350d5041 Fix unable to patch images when app is hidden 2021-02-07 06:42:06 -08:00
topjohnwu
f37e8f4ca8 Fix boot image patching 2021-02-07 01:54:08 -08:00
topjohnwu
594c2accc0 Update dependencies 2021-02-05 04:41:01 -08:00
topjohnwu
7acfac6a91 Publish new canary build 2021-01-30 12:54:51 -08:00
Laz M
0646f48e14 [README] Warn users that the official website is github
Google puts a number of cheeky looking websites in the results for Magisk.

I only found out they were unofficial is though issue #3435. This deserves to be shown more prominently.
2021-01-30 11:59:39 -08:00
tzagim
37565fd067 Fix TYPOs 2021-01-30 11:58:43 -08:00
vvb2060
26b2e7dc5d Care version code changes 2021-01-30 11:58:10 -08:00
vvb2060
c3313623e4 Fix release build 2021-01-30 11:58:10 -08:00
topjohnwu
2089223690 Fix #3785 2021-01-30 11:51:15 -08:00
topjohnwu
52e1b84d41 Symlink pre API 21 2021-01-30 01:12:49 -08:00
topjohnwu
8794141b7f Support super old emulators 2021-01-30 00:56:16 -08:00
topjohnwu
f6126dd20e Support Shortcuts pre API 26
Close #3778
2021-01-29 23:16:09 -08:00
topjohnwu
18acfda99b Remove Windows NDK symlink
https://github.com/actions/virtual-environments/pull/2343
2021-01-29 05:43:41 -08:00
topjohnwu
bec5edca84 Avoiding using shell I/O 2021-01-29 05:15:22 -08:00
topjohnwu
6fb20b3ee5 Proper proguard rules 2021-01-27 04:56:39 -08:00
topjohnwu
eaf4d8064b Also download to external storage 2021-01-27 04:09:07 -08:00
topjohnwu
2a5f5b1bba Workaround zip extraction bug on older devices 2021-01-27 03:00:09 -08:00
topjohnwu
c538a77937 Tweak build configs and scripts 2021-01-27 02:36:32 -08:00
sominn
aa9e7b1ed1 Update strings.xml
CS string update
2021-01-27 01:00:10 -08:00
Arbri çoçka
a3066eddab Fix string in values-sq 2021-01-27 00:59:49 -08:00
Arbri çoçka
d1729fa787 Fix string in values-sq 2021-01-27 00:59:49 -08:00
vvb2060
93961dde2c Fix version on continuous build 2021-01-27 00:54:11 -08:00
topjohnwu
1024e68eb6 Remove class mapping in full APK 2021-01-26 07:27:35 -08:00
topjohnwu
6ae2c9387d Use stub APK hiding method for Android 5.0+
At the same time, disable app hiding on devices lower than 5.0
to simplify the logic in the app. By doing so, a hidden app always
implies running as stub.
2021-01-26 07:27:35 -08:00
topjohnwu
fba83e2330 Support stub APK loading down to Android 5.0 2021-01-26 07:27:35 -08:00
topjohnwu
f1295cb7d6 Fix root on Android 7.0 and lower 2021-01-26 02:16:11 -08:00
topjohnwu
dc61dfbde6 Cache update check results 2021-01-25 04:13:08 -08:00
topjohnwu
21466426da Some code cleanup 2021-01-25 03:44:38 -08:00
topjohnwu
3f0136362b Move nand flash handling into boot_patch.sh 2021-01-25 03:37:41 -08:00
topjohnwu
e92d77bbec Some optimizations 2021-01-25 03:02:43 -08:00
topjohnwu
07bd36c94b Fix patching files
Fix #3765
2021-01-25 02:24:12 -08:00
topjohnwu
b1dbbdef12 Remove unneeded busybox redirection 2021-01-25 00:23:42 -08:00
topjohnwu
3e479726ec Fix legacy rootfs devices 2021-01-25 00:19:10 -08:00
vvb2060
4cc41eccb3 Skip download notes when loading notes url 2021-01-24 21:02:51 -08:00
vvb2060
8f08ae59ac Fix permission 2021-01-24 21:02:43 -08:00
vvb2060
e8d4e492d6 Fix CHANGELOG_URL 2021-01-24 21:02:37 -08:00
topjohnwu
b8090a8e18 Ensure cwd is writable in module scripts
Close #3763
2021-01-24 20:58:30 -08:00
topjohnwu
c609a01e55 Proper shortcut name 2021-01-24 08:00:17 -08:00
Wagg13
c97fb385cd New update values-pt-rBR
update brazilian strings.xml
2021-01-24 07:36:07 -08:00
LLZN
da6c57750e correction czech translat
change and fix some strings after trying a new version of the application (v8.0.6)
2021-01-24 07:35:49 -08:00
topjohnwu
6951d926f7 Rename app name to just Magisk 2021-01-24 07:35:00 -08:00
topjohnwu
6623195bd5 Cleanup scripts 2021-01-24 07:24:13 -08:00
topjohnwu
ec31bb9a82 Rename scripts 2021-01-24 07:18:14 -08:00
vvb2060
8618cc383a Fix install modules
Fix #3759
2021-01-24 07:03:19 -08:00
vvb2060
4b01e3a3c7 Cleanup more kotlin stuffs 2021-01-24 07:03:06 -08:00
vvb2060
7f748c23c1 Use Java debugger 2021-01-24 07:02:44 -08:00
vvb2060
963d248cc7 Rename apk to be uninstaller 2021-01-24 07:02:36 -08:00
topjohnwu
657056e636 Cache changelog files 2021-01-24 06:55:43 -08:00
topjohnwu
9d5efea66e Remove ManagerJson
Everything is now Magisk
2021-01-24 05:14:46 -08:00
topjohnwu
658d74e026 Update home fragment 2021-01-24 00:02:49 -08:00
vvb2060
5113f6d375 Fix stop magiskhide 2021-01-23 18:13:15 -08:00
vvb2060
96405c26d0 writeTo has closed InputStream 2021-01-23 18:12:19 -08:00
vvb2060
4ea5f34bf3 Remove unused action 2021-01-23 18:11:08 -08:00
vvb2060
dbd13a2019 Clean code 2021-01-23 18:10:26 -08:00
vvb2060
06773235da Fix Windows build 2021-01-23 18:06:01 -08:00
vvb2060
e57556a8af Use ro.kernel.qemu to check emulator 2021-01-23 18:05:38 -08:00
vvb2060
b54b78c29d Fix prevent dot in the first position 2021-01-23 17:31:18 -08:00
vvb2060
317336f771 Add isolated processes log 2021-01-23 17:31:11 -08:00
topjohnwu
b4e52f6135 Better development workflow 2021-01-23 16:50:55 -08:00
topjohnwu
f2ca042915 Fix script for handling .apex files 2021-01-23 16:09:30 -08:00
topjohnwu
1060dd2906 Random refactoring 2021-01-23 13:26:28 -08:00
topjohnwu
2e0f7a82fa More complete stub sources 2021-01-22 20:45:37 -08:00
topjohnwu
5798536559 Remove unnecessary hacks 2021-01-22 20:25:37 -08:00
topjohnwu
ab9a83c82f Bump target SDK to 30 2021-01-22 05:03:33 -08:00
topjohnwu
c87fdbea0f Fix erroneous stream close 2021-01-22 03:07:39 -08:00
topjohnwu
ec8fffe61c Merge Magisk install zip into Magisk Manager
Distribute Magisk directly with Magisk Manager APK. The APK will
contain all required binaries and scripts for installation and
uninstallation. App versions will now align with Magisk releases.

Extra effort is spent to make the APK itself also a flashable zip that
can be used in custom recoveries, so those still prefer to install
Magisk with recoveries will not be affected with this change.

As a bonus, this makes the whole installation and uninstallation
process 100% offline. The existing Magisk Manager was not really
functional without an Internet connection, as the installation process
was highly tied to zips hosted on the server.

An additional bonus: since all binaries are now shipped as "native
libraries" of the APK, we can finally bump the target SDK version
higher than 28. The target SDK version was stuck at 28 for a long time
because newer SELinux restricts running executables from internal
storage. More details can be found here: https://github.com/termux/termux-app/issues/1072
The target SDK bump will be addressed in a future commit.

Co-authored with @vvb2060
2021-01-22 02:29:54 -08:00
topjohnwu
61d52991f1 Update BusyBox 2021-01-21 00:35:22 -08:00
topjohnwu
9100186dce Make emulator direct install env fix 2021-01-18 13:32:10 -08:00
topjohnwu
d2bc2cfcf8 Install both 32 and 64 bit binaries 2021-01-18 12:37:08 -08:00
topjohnwu
5a71998b4e Stop embedding magisk in magiskinit 2021-01-18 04:25:26 -08:00
topjohnwu
42278f12ff Fix typo in init daemon 2021-01-18 04:13:54 -08:00
topjohnwu
f5593e051c Update README 2021-01-17 06:19:56 -08:00
topjohnwu
a27e30cf54 Update release notes 2021-01-17 06:08:15 -08:00
topjohnwu
79140c7636 Proper xxread and xwrite implementation 2021-01-17 01:42:45 -08:00
topjohnwu
1f4c595cd3 Revert to old su -c behavior 2021-01-16 23:59:31 -08:00
topjohnwu
b5b62e03af Fix copySepolicyRules logic 2021-01-16 21:45:45 -08:00
topjohnwu
67e2a4720e Fix xxread false negatives
Fix #3710
2021-01-16 21:43:53 -08:00
topjohnwu
f5c2d72429 Also log pid and tid 2021-01-16 16:10:47 -08:00
topjohnwu
2f5331ab48 Update README 2021-01-16 05:02:39 -08:00
topjohnwu
7f8257152f Add v21.3 release notes 2021-01-16 04:55:44 -08:00
topjohnwu
0cd80f2556 Update app changelog 2021-01-16 04:42:14 -08:00
rydwhelchel
1717387876 Grammatical changes to the install docs 2021-01-15 21:32:29 -08:00
Mspy1
109363ebf6 Fixed typo 2021-01-15 21:31:58 -08:00
LLZN
716c4fa386 new update values-cs
update czech strings.xml
2021-01-15 21:31:17 -08:00
Arbri çoçka
9a09b4eb20 fix strings-sq 2021-01-15 21:29:53 -08:00
Rikka
95a5b57265 Remove "Flashing" overlay
Fix #3579, fix #3250
2021-01-15 21:28:59 -08:00
topjohnwu
13fbf397d1 Isolated processes might still be hide-able 2021-01-15 20:22:49 -08:00
vvb2060
20be99ec8a Restore mistakenly deleted codes 2021-01-15 19:59:55 -08:00
topjohnwu
04c53c3578 Legacy SAR: use a simpler method to detect is_two_stage 2021-01-15 02:44:40 -08:00
topjohnwu
51bc27a869 Avoid F2FS like a plague 2021-01-15 02:24:11 -08:00
topjohnwu
71b083794c Maintain global mount list 2021-01-14 21:14:54 -08:00
topjohnwu
b100d0c503 Revert DTB fstab changes 2021-01-14 19:48:00 -08:00
topjohnwu
76061296c9 Let MagiskBoot handle dtb fstab patching 2021-01-14 06:20:12 -08:00
topjohnwu
bb303d2da1 Remove old unused code 2021-01-14 05:59:53 -08:00
topjohnwu
c91c070343 Re-enable DTB table rebuilding 2021-01-14 05:45:05 -08:00
topjohnwu
aec06a6f61 Get proper total image size 2021-01-14 03:55:27 -08:00
topjohnwu
e8ba671fc2 Guard all injection features behind a global flag 2021-01-13 20:07:23 -08:00
topjohnwu
1860e5d133 Dynamically find libselinux.so path 2021-01-13 19:41:57 -08:00
topjohnwu
f2cb3c38fe Update mmap implementation
Always map memory as writable, but private when read-only
2021-01-12 22:50:55 -08:00
topjohnwu
9a28dd4f6e Implement MagiskHide through code injection 2021-01-12 03:28:00 -08:00
topjohnwu
d2acd59ea8 Minor code refactoring 2021-01-12 00:07:48 -08:00
topjohnwu
79dfdb29e7 Minor tweaks for patching tar files 2021-01-11 19:47:36 -08:00
topjohnwu
eb21c8b42e Code cleanups 2021-01-11 02:19:10 -08:00
topjohnwu
541bb53553 Update links in README 2021-01-10 20:36:58 -08:00
Hen Ry
fe8997efae Fix 2021-01-10 20:17:20 -08:00
Arbri çoçka
23455c722c fix in Values-sq 2021-01-10 20:16:57 -08:00
topjohnwu
5ce29c30d2 Fix sepolicy copying 2021-01-10 20:16:02 -08:00
topjohnwu
70d67728fd Add global toggle for ptrace monitor 2021-01-10 19:27:54 -08:00
topjohnwu
e546884b08 Remove isolated process handling in ptrace
Impossible to achieve only through ptrace
2021-01-10 17:18:42 -08:00
topjohnwu
b36e6d987d Reorganize MagiskHide code
Prepare for zygote injection hiding
2021-01-10 17:11:00 -08:00
topjohnwu
53c3dd5e8b Auto track JNI method hooks 2021-01-10 05:07:17 -08:00
topjohnwu
da723b207a Allow 3rd party code to load pre-specializing
Magisk's policy is to never allow 3rd party code to be loaded in the
zygote daemon process so we have 100% control over injection and hiding.
However, this makes it impossible for 3rd party modules to run anything
before process specialization, which includes the ability to modify the
arguments being sent to these original nativeForkAndXXX methods.

The trick here is to fork before calling the original nativeForkAndXXX
methods, and hook `fork` in libandroid_runtime.so to skip the next
invocation; basically, we're moving the responsibility of process
forking to our own hands.
2021-01-10 01:25:30 -08:00
topjohnwu
e050f77198 Don't hook SystemProperties#set
Doesn't seem necessary
2021-01-09 20:39:59 -08:00
topjohnwu
540b4b7ea9 Update pre/post hooks implementation 2021-01-09 17:41:25 -08:00
topjohnwu
bbef22daf7 More macro magic to automate more code 2021-01-09 04:28:26 -08:00
topjohnwu
9ed110c91b Add JNI hooks to critical methods 2021-01-08 05:25:44 -08:00
topjohnwu
a30d510eb1 Use xHook to hook functions in PLT 2021-01-08 00:53:24 -08:00
topjohnwu
ef98eaed8f Proper injection entry and unloading 2021-01-06 23:59:05 -08:00
topjohnwu
2a257f327c Sanitize /proc/PID/environ 2021-01-06 23:41:37 -08:00
topjohnwu
4060c2107c Add preliminary zygote code injection support
Prototyping the injection setup and a clean "self unloading" mechanism.
2021-01-06 22:21:17 -08:00
topjohnwu
cd23d27048 Fix remote_write implementation 2021-01-06 21:56:29 -08:00
topjohnwu
18b86e4fd2 Update Android.mk for test binary
Make Android Studio happy
2021-01-05 00:01:02 -08:00
topjohnwu
5f2e22a259 Support remote function call with ptrace
End up not used for anything, but keep it for good
2021-01-02 21:29:45 -08:00
topjohnwu
4e97b18977 Move libsystemproperties to external 2020-12-31 15:06:19 -08:00
topjohnwu
f9bde347bc Convert indentation to spaces
The tab war is lost
2020-12-30 22:11:24 -08:00
Billy Laws
947a7d6a2f Support rootwait cmdline parameter on legacy SAR
On devices where the primary storage is slow to probe it makes sense to
wait forever for the system partition to mount, this emulates the
kernel's behaviour when waiting for rootfs on SAR if the rootwait
parameter is supplied.

This issue was encountered with some SD cards on the Nintendo Switch.
2020-12-30 16:43:28 -08:00
Björn Engel
872ab2e99b Change translation for next
Nächste sounds a little bit strange.
2020-12-30 16:41:22 -08:00
kubalav
90b8813bb7 Fixed typo 2020-12-30 16:41:01 -08:00
Arbri çoçka
88d0f63294 Fix text in strings_sq 2020-12-30 16:40:47 -08:00
topjohnwu
79fa0d3a90 Hide selection improvements 2020-12-30 16:40:22 -08:00
topjohnwu
8e61080a4a Preparation for hiding isolated processes 2020-12-30 15:55:53 -08:00
topjohnwu
3f9a64417b Disable gradle daemon on Windows CI 2020-12-29 02:46:57 -08:00
topjohnwu
eb959379e8 Prevent resource ID clash 2020-12-29 02:39:47 -08:00
topjohnwu
41a644afb9 Open source stub APK loader
Close #3537
2020-12-29 01:44:02 -08:00
topjohnwu
6b42db943d Better bug report details 2020-12-28 17:03:20 -08:00
topjohnwu
1c325459eb Only run CI when it matters 2020-12-28 16:38:25 -08:00
John Wu
6d88d8ad95 Add issue templates 2020-12-28 16:26:10 -08:00
topjohnwu
246997f273 Update links 2020-12-28 15:58:53 -08:00
topjohnwu
b6144ae582 Add v21.2 release notes 2020-12-28 15:35:09 -08:00
Arbri çoçka
afe17c73b4 Update strings.xml
Fix same text in Values-sq
2020-12-28 15:29:27 -08:00
topjohnwu
b51b884fc7 Fix module installs in recovery
Close #3494
2020-12-28 00:25:01 -08:00
topjohnwu
d3e4b29e62 Update README.md 2020-12-27 22:36:03 -08:00
dark-basic
24059e7403 Update Stub-es version 2020-12-27 22:09:03 -08:00
dark-basic
107a2a6682 Update String-es 2020-12-27 22:08:34 -08:00
Arbri çoçka
56b4ab6672 Fix any text in strings sq 2020-12-27 22:07:44 -08:00
topjohnwu
4662454938 More attempts to fix gradle cache on Windows 2020-12-27 20:13:50 -08:00
topjohnwu
db4f78d463 Unblock signals before executing commands 2020-12-27 15:05:39 -08:00
topjohnwu
880de21596 Update Github Actions
Disable gradle daemon on Windows and always upload artifacts
2020-12-27 04:02:20 -08:00
topjohnwu
622dd84c9e Fix uninstaller zip 2020-12-26 22:45:05 -08:00
topjohnwu
f983bfc883 Embed keys into dex files 2020-12-26 21:33:30 -08:00
topjohnwu
45cdb3fdb0 Update dependencies 2020-12-26 17:05:12 -08:00
topjohnwu
9a707236b8 Move signing code into main app sources 2020-12-26 17:03:10 -08:00
topjohnwu
e9e6ad3bb0 Sign zips with apksigner 2020-12-26 16:04:41 -08:00
topjohnwu
ab78a81d15 Fix GitHub actions 2020-12-25 15:54:47 -08:00
John Wu
18340099b7 Add GitHub actions
Enable GitHub actions to run CI on all 3 platforms
2020-12-25 15:01:02 -08:00
topjohnwu
a013696a41 Default to config.prop in buildSrc 2020-12-25 13:03:25 -08:00
topjohnwu
8a2a6d9232 Make versionCode unconfigurable 2020-12-25 05:34:15 -08:00
topjohnwu
12aa6d86e4 Make config.prop optional 2020-12-24 04:46:31 -08:00
topjohnwu
7d08969d28 Fix strings 2020-12-23 01:33:46 -08:00
Fs00
dda4aa8488 Translate missing Italian strings 2020-12-22 23:33:20 -08:00
binarynoise
cdaef3d801 Update install.md
I wanted to share my experiences with rooting my S10e.
2020-12-22 23:32:53 -08:00
amninder singh
9159166128 Update strings.xml
Updating strings.xml
regarding #3566 adding punjabi translation
2020-12-22 23:32:01 -08:00
Arbri çoçka
dc0882e043 fixing some errors in sq strings 2020-12-22 23:31:40 -08:00
amninder singh
c811f015ef Added Punjabi Translation
- Written in Gurmukhi Script containing different foreign punctuations both inscript and Phonetic
2020-12-22 23:31:12 -08:00
nkh0472
d8f0b66fe1 Improve Correctness and Clarity of guides.md 2020-12-22 23:30:37 -08:00
Mohd Faraz
dc3d57deba utils_functions: Added a check for the system_root
now on addon while flashing recovery usign mount point /system_root by which this is causing a flashing error.
Let's first check and unmount /system_root if mounted

Signed-off-by: Mohd Faraz <androiabledroid@gmail.com>
2020-12-22 23:30:14 -08:00
topjohnwu
d089698475 Don't use root for logging getprop
Might contain sensitive info that shouldn't be shared
2020-12-19 23:09:36 -08:00
vvb2060
8ed2dd6687 Skip query for log files and patched boot file 2020-12-19 22:26:10 -08:00
vvb2060
50305ca1fe Support save manager log 2020-12-19 22:25:44 -08:00
vvb2060
3e91567636 Add a suffix to magisk_patched.img
prevent it from being used as input file
2020-12-19 21:53:16 -08:00
vvb2060
0b4dd63d36 Stub module always use release build 2020-12-19 21:02:05 -08:00
vvb2060
38d0f85deb Avoid unnecessary builds 2020-12-19 15:57:11 -08:00
vvb2060
c5b452f369 Get boot config properly
https://android.googlesource.com/platform/system/core/+/refs/tags/android-11.0.0_r16/fs_mgr/fs_mgr_boot_config.cpp#93
2020-12-19 15:55:33 -08:00
vvb2060
6ce9225f52 Check block dev ro status
magisk is shared object, use static busybox instead
2020-12-19 14:12:12 -08:00
vvb2060
13a8820603 Double check $DATA_DE 2020-12-19 14:08:32 -08:00
vvb2060
503997a09a Trim out \r 2020-12-19 14:08:06 -08:00
vvb2060
17efdff134 remove_system_su only on recovery mode
We may mount su to /system/bin/su
2020-12-19 13:59:08 -08:00
vvb2060
984f32f994 Move copy_sepolicy_rules to manager
We don’t need it when patch boot
2020-12-19 13:58:53 -08:00
topjohnwu
eee7f097e3 Make post-fs-data scripts block at most 35 secs 2020-12-17 16:54:53 -08:00
topjohnwu
086059ec30 Make sure boot stages are mutually exclusive 2020-12-15 03:40:37 -08:00
topjohnwu
7ff22c68c7 Only try to install APK when no manager is active 2020-12-09 02:15:16 -08:00
topjohnwu
1232113772 Update preference migration implementation
Only try to read preference through content provider when the app
is fresh install and a previous package ID is set. Also catch all
Exceptions to prevent crashing the app.

This prevents malicious settings injection and crashes when multiple
manager is installed.

Fix #3542
2020-12-09 02:07:58 -08:00
vvb2060
039d4936cb Disable superuser fragment properly 2020-12-09 01:16:30 -08:00
topjohnwu
784dd80965 Update MediaStoreUtils 2020-12-09 01:15:56 -08:00
vvb2060
1ffe9bd83b Fix update channel without description on release build 2020-12-09 00:40:33 -08:00
topjohnwu
0c28b23224 Fix install_module command 2020-12-09 00:37:15 -08:00
vvb2060
ec1af9dc1e Delete useless arrays 2020-12-09 00:17:58 -08:00
vvb2060
ff4cea229a Check environment on emulator
We need to test modules on emulator.
2020-12-09 00:16:27 -08:00
vvb2060
3f81f9371f Disable installation while downloading metadata 2020-12-09 00:16:13 -08:00
vvb2060
60e89a7d22 Ignore manager not installed state 2020-12-09 00:15:58 -08:00
vvb2060
c50daa5c9e Allow restore boot when no network 2020-12-09 00:15:41 -08:00
topjohnwu
58d00ab863 Remove some warnings 2020-12-07 00:06:02 -08:00
topjohnwu
ce916459c5 Fix strings 2020-12-07 00:04:41 -08:00
Antikruk
4094d560ab Belarusian update 2020-12-06 23:50:33 -08:00
RikkaW
4dbf7eb04b Fix spacing in module filter list 2020-12-06 23:49:23 -08:00
RikkaW
a39577c44d Fix extra spacing in module list 2020-12-06 23:49:23 -08:00
osm0sis
125ee46685 scripts: fix find_manager_apk
- the strings fallback was broken when the preceding character changed from 5 to ! recently, this new regex should cover any preceding character going forward
2020-12-06 23:49:03 -08:00
osm0sis
ce84f1762c scripts: fix sloppy/unpredictable cmd && this || that statements
- be a bit more POSIX to avoid any potential issues when full shell stdout/err are redirected
- actual logic chains remain unchanged
2020-12-06 23:48:43 -08:00
Hafitz Setya
a687d1347b Tidying up IN 2020-12-06 23:48:01 -08:00
Arbri çoçka
6d9db20614 Create strings.xml 2020-12-06 23:46:18 -08:00
topjohnwu
c62dfc1bcc Make logging less error prone 2020-12-06 23:09:24 -08:00
topjohnwu
aabe2696fe Cleanup implementations 2020-12-06 03:07:47 -08:00
topjohnwu
ae0d605310 Make sure patch_rootdir does not cause crashes 2020-12-06 02:19:57 -08:00
topjohnwu
2a694596b5 Better error handling and logging 2020-12-05 10:23:49 -08:00
topjohnwu
ff0a76606e Detect 2SI after system_root mount on legacy SAR 2020-12-04 03:06:21 -08:00
topjohnwu
dead74801d Setup log file when manually starting daemon 2020-12-04 01:07:47 -08:00
topjohnwu
ab207a1bb3 va_list cannot be reused on x86 2020-12-03 20:53:19 -08:00
topjohnwu
f152e8c33d Directly log to log file 2020-12-03 20:15:18 -08:00
topjohnwu
797ba4fbf4 Make sure all logging ends with newline 2020-12-02 00:55:22 -08:00
topjohnwu
a848f10bba Update Kotlin 2020-11-23 12:35:24 -08:00
topjohnwu
552ec1eb35 Header v3 does not have name entry 2020-11-20 22:52:09 -08:00
topjohnwu
1385d2a4f4 Fix strings 2020-11-19 23:34:02 -08:00
RikkaW
3b5c9abf7a Remove filterTouchesWhenObscured in Magisk dialog
Fix #3363
2020-11-19 23:32:23 -08:00
tzagim
e0fa032bd3 Update HE strings and fix typos 2020-11-19 23:29:47 -08:00
omerakgoz34
7b69650fcd app: Update Turkish translations 2020-11-19 23:27:54 -08:00
kubalav
08a8df489f Slovak language formating 2020-11-19 23:27:07 -08:00
cristisilaghi
9f35a8a520 Update Romanian 2020-11-19 23:26:38 -08:00
RikkaW
0df891b336 Handle window insets with a new way
For example, switching pages in home should only have scale and alpha animations, but a "translate y" animation shows. This is because Data Binding is triggered later (like "in the next frame"), causing the animation runs before view attribute changes.

This commit introduces WindowInsetsHelper class and use it to handle all window insets. With the help of LayoutInflaterFactory from the previous commit, we can control insets behavior by adding our attributes to the XML and anything is done by WindowInsetsHelper class.

As changes are highly coupling, this commit also contains new ItemDecoration for lists, replacing the random combination of padding and empty drawable. And "fixEdgeEffect" extension for RecyclerView, making edge effects respect padding.
2020-11-19 23:24:39 -08:00
RikkaW
385853a290 Introduce LayoutInflaterFactory
This add the ability touch layout XML instantiates process. And most importantly, we can access AttributeSet, making custom view attribute possible.

Some other changes requires this.
2020-11-19 23:24:39 -08:00
RikkaW
fa3ef8a1c1 Significantly simplify MagiskDialog layout
The goal of original implementation, wrap view again and again, seems to be use the shadow and customizable round corners from MaterialCardView. But this can be done with use MaterialShapeDrawable which used in MaterialCardView directly. This will significantly simplify the layout and MagiskDialog class.
2020-11-19 23:21:36 -08:00
RikkaW
c93ada03c7 Implement Edge-to-edge with newer APIs
The implementation adds a "Base" family styles, making creating themes across multiple API versions more clearer and easier.
2020-11-19 23:21:36 -08:00
topjohnwu
0064b01ae0 Trim out \r from string
Fix #3490
2020-11-15 06:30:29 -08:00
topjohnwu
1469b82aa2 Update README 2020-11-13 04:38:17 -08:00
topjohnwu
2d5cf8a6fe Push release notes 2020-11-13 04:32:20 -08:00
topjohnwu
290959f74c Fix strings resources 2020-11-13 04:22:06 -08:00
Ilya Kushnir
4d9f58ee72 Update RU strings & tidying up EN 2020-11-13 03:03:35 -08:00
topjohnwu
9241246de6 Only use MediaStore APIs on Android 11+
Fix #3428
2020-11-13 02:53:30 -08:00
Heimen Stoffels
58a5d52b78 Updated Dutch translation 2020-11-13 02:34:49 -08:00
Rom
2906178ac3 Update French translation 2020-11-13 02:34:21 -08:00
topjohnwu
e0afbb647b Minor changes 2020-11-13 02:31:54 -08:00
topjohnwu
50be50cf6a Update dependencies 2020-11-13 00:58:41 -08:00
topjohnwu
77a9d3a5bc Upgrade AGP 2020-11-12 23:29:07 -08:00
topjohnwu
f9c7a4c933 Redirect /data/adb/magisk/busybox
Workaround some stupid Samsung kernel restrictions
2020-11-11 02:26:07 -08:00
topjohnwu
2b759b84b0 Properly reset string 2020-11-09 21:17:21 -08:00
topjohnwu
1e45c63ea5 Scan for zygote periodically
Fix #3417
2020-11-08 03:44:43 -08:00
topjohnwu
b14a260827 Offset pid_set by 1
PID starts at 1, not 0
2020-11-08 02:12:35 -08:00
topjohnwu
ade1597e03 Support hiding apps not installed in main user
Fix #2181, close #1840
2020-11-08 01:53:18 -08:00
topjohnwu
2739d3cb67 Update PayPal link 2020-11-07 15:10:10 -08:00
cheese1
dc5e78e142 Fix German Translation Typo 2020-11-07 14:48:22 -08:00
vvb2060
e9759a5868 Update HideViewModel 2020-11-07 14:47:44 -08:00
AdiityaAndre
e7ab802498 Update Indonesian translation 2020-11-07 14:43:46 -08:00
kubalav
42672c2e27 Update Slovak translation 2020-11-07 14:43:21 -08:00
孟武.尼德霍格.龍
e65d61d313 更新繁體中文字串
更新繁體中文字串
2020-11-07 14:42:48 -08:00
Taras
076da5c7c4 Update Ukrainian translation 2020-11-07 14:42:11 -08:00
vvb2060
9deaf2507c Update zh-rCN translation 2020-11-07 14:41:32 -08:00
kam821
5c114c67de Update Polish translation
- Add missing strings
- Small corrections.
- Changed "Magisk Manager" translation to form which better reflects the original meaning in Polish.
2020-11-07 14:40:56 -08:00
programminghoch10
d904cb0441 Updated german translations 2020-11-07 14:40:02 -08:00
pablomh
bd1dd9d863 Fix ensure_bb by assigning the arguments propery
If we assign the execution output directly it will fail (tested on Android 11):

pdx201:/ # INSTALLER=/data/adb/magisk_install /data/adb/magisk_install/flash_script.sh                                                                          
/data/adb/magisk_install/flash_script.sh[31]: typeset: -o: is not an identifier

Because:

local cmds=$($bb sh -o standalone -c "
	  for arg in \$(tr '\0' '\n' < /proc/$$/cmdline); do
	    if [ -z \"\$cmds\" ]; then
	      # Skip the first argument as we want to change the interpreter
	      cmds=\"sh -o standalone\"
	    else
	      cmds=\"\$cmds '\$arg'\"
	    fi
	  done
	  echo \$cmds")
/system/bin/sh: typeset: -o: is not an identifier

Signed-off-by: Pablo Mendez Hernandez <pablomh@gmail.com>
2020-11-07 14:39:03 -08:00
topjohnwu
afebe734b8 Fix several things regarding scripting 2020-11-07 14:36:13 -08:00
topjohnwu
e21a78164e Properly handle factory resets
Close #3345
2020-11-04 04:50:04 -08:00
topjohnwu
1e0f96d0fd Prefer platform implementation over internal 2020-11-04 04:42:02 -08:00
topjohnwu
bf650332d8 Update nanopb 2020-11-04 01:56:49 -08:00
topjohnwu
f32e0af830 Update resetprop help msg 2020-11-03 01:21:05 -08:00
topjohnwu
4c94f90e5d Templatize function callbacks 2020-11-03 01:16:55 -08:00
topjohnwu
ffb4224640 Don't use reserved symbols 2020-11-03 01:12:33 -08:00
topjohnwu
89fff4830b Mount proper system_root mirror in magiskd 2020-11-03 00:37:08 -08:00
topjohnwu
16e4c67992 Significantly broaden sepolicy.rule compatibility
Previously, Magisk uses persist or cache for storing modules' custom
sepolicy rules. In this commit, we significantly broaden its
compatibility and also prevent mounting errors.

The persist partition is non-standard and also critical for Snapdragon
devices, so we prefer not to use it by default.

We will go through the following logic to find the best suitable
non-volatile, writable location to store and load sepolicy.rule files:

Unencrypted data -> FBE data unencrypted dir -> cache -> metadata -> persist

This should cover almost all possible cases: very old devices have
cache partitions; newer devices will use FBE; latest devices will use
metadata FBE (which guarantees a metadata parition); and finally,
all Snapdragon devices have the persist partition (as a last resort).

Fix #3179
2020-11-02 23:20:38 -08:00
topjohnwu
cf47214ee4 Require Magisk v20.4 for modules
It has been long enough
2020-10-28 05:13:39 -07:00
topjohnwu
0feab753fb Fix coding errors and minor changes 2020-10-28 04:17:34 -07:00
Leorize
d0b6318b90 init/mount: support for dm-verity verified root
This commit adds support for kernel initialized dm-verity on legacy SAR
devices.

Tested on a Pixel 2 XL with a kernel patch to initialize mappings
specified via the `dm=` kernel parameter even when an initramfs is used.
2020-10-27 03:47:50 -07:00
topjohnwu
966e23b846 magiskinit code tidy-up 2020-10-26 20:46:15 -07:00
topjohnwu
5b8a1fc2a7 Minor renames 2020-10-25 21:41:14 -07:00
topjohnwu
02ea3ca525 Headers doesn't always occupy 1 page 2020-10-25 06:25:42 -07:00
topjohnwu
0632b146b8 Add vendor boot image support to magiskboot 2020-10-25 06:09:36 -07:00
topjohnwu
1b0b180761 Fix COMPRESSED macro
Fix #3383
2020-10-25 05:10:19 -07:00
topjohnwu
0d11f73a1d Handle unexpected exceptions
Fix #3276
2020-10-22 03:09:05 -07:00
vvb2060
533cb8eb58 Tapjacking protection 2020-10-22 02:40:47 -07:00
loading
8ac1181e9a Update Hindi translations 2020-10-21 00:43:15 -07:00
Ilya Kushnir
5ca1892eb0 Update RU strings 2020-10-21 00:42:09 -07:00
MASVA
e32db6a0e8 Update croatian language 2020-10-21 00:41:48 -07:00
kubalav
82fff615d6 Update Slovak translation 2020-10-21 00:40:36 -07:00
Rom
24a8f0808d Update French translation 2020-10-21 00:40:08 -07:00
vvb2060
4a7c3c06bc Disable hide/restore when no remote info 2020-10-20 23:56:44 -07:00
vvb2060
da93bbc1fe Fix network 2020-10-20 23:56:21 -07:00
topjohnwu
fa2dbe981e Handle retrofit errors 2020-10-20 03:03:40 -07:00
vvb2060
ce6cceae8b Smaller stub 2020-10-17 06:55:24 -07:00
topjohnwu
7b26e8b818 Update dependencies 2020-10-17 06:46:36 -07:00
topjohnwu
2da5fcb00b ANDROID_HOME is deprecated 2020-10-17 06:42:34 -07:00
topjohnwu
a079966f97 Migrate to AGP 4.1.0 2020-10-17 06:32:49 -07:00
vvb2060
468796c23d Add option to show OS apps 2020-10-17 05:57:43 -07:00
vvb2060
5833aadef5 Silence kotlin warnings 2020-10-17 05:57:35 -07:00
vvb2060
eb261c8026 Fix antlr warning
https://issuetracker.google.com/issues/150106190
2020-10-17 05:57:20 -07:00
vvb2060
a4c48847d1 Cancel vibration to sync with notification channel 2020-10-17 05:56:07 -07:00
vvb2060
43288be091 Prevent dot in the first position 2020-10-17 05:55:58 -07:00
vvb2060
1ad7a6fe93 Update activity display when download fails 2020-10-17 05:54:34 -07:00
topjohnwu
4e0a3f5e72 Fix compile errors 2020-10-17 04:28:20 -07:00
Davy Defaud
d7c33f647d Fix a typo and use the proper Unicode characters
- fix a French typo: raccourcis → raccourci
- French orthotypography: use a thin space before a question mark, and a true (non breaking) hyphen instead of a dash.
2020-10-17 04:18:00 -07:00
topjohnwu
9087207dc0 Minor changes 2020-10-17 04:14:12 -07:00
vvb2060
2760f37e6b Add userspace reboot 2020-10-17 03:54:51 -07:00
Miguel Cruces
3fa3426032 Spanish translations update 2020-10-17 03:54:17 -07:00
topjohnwu
2e4dc91b96 Better stub hiding experience 2020-10-17 03:40:43 -07:00
topjohnwu
aaaaa3d044 Minor refactoring 2020-10-15 00:19:11 -07:00
topjohnwu
1edc4449d5 Update lz4 to v1.9.2
Close #3334
2020-10-15 00:04:48 -07:00
topjohnwu
f3cd4da026 Make lz4_lg an exception of lz4_legacy 2020-10-14 23:45:06 -07:00
vvb2060
872c55207c Add com.android.i18n to apex path 2020-10-12 01:59:41 -07:00
topjohnwu
339ca6d666 Improve magiskboot info logging 2020-10-12 01:55:33 -07:00
topjohnwu
4aeac3b8f4 Support header_version 3 2020-10-12 01:06:42 -07:00
topjohnwu
d625beb7f3 Update --remove-modules implementation 2020-10-11 18:30:03 -07:00
topjohnwu
735b65c50c Update DoH implementation 2020-10-11 15:19:19 -07:00
topjohnwu
efb1eab327 Silence some warnings 2020-10-11 05:47:47 -07:00
topjohnwu
49d4785da0 Fix strings 2020-10-11 05:26:50 -07:00
RoySchutte
28e65ce383 Update strings.xml
I don't know who else is translating Magisk Manager, but I noticed some weird translations. Fixed a couple of them in this update.
2020-10-11 05:24:16 -07:00
Antikruk
c3b6a48373 belarusian 2020-10-11 05:23:51 -07:00
omerakgoz34
a42ebd429b Update Turkish(TR) Translation 2020-10-11 05:22:01 -07:00
MASVA
8f89010752 Update croatian language 2020-10-11 05:20:34 -07:00
GrepItAll
105a18f719 Temporary note about OTA update no longer working
Added temporary note about OTA update no longer working, as this option has been disabled in newer Magisk Manager versions
2020-10-11 05:19:38 -07:00
topjohnwu
eb04ca4c4a Make provider boot aware
Close #3322
2020-10-11 05:19:05 -07:00
topjohnwu
6092d7ca88 Minor cleanups 2020-10-11 05:10:02 -07:00
topjohnwu
66cad101c0 Support new canary links 2020-10-11 03:37:03 -07:00
topjohnwu
0a14f43f9c Refactor class names 2020-10-10 22:40:57 -07:00
topjohnwu
311c1f0dfd Switch to new repo format 2020-10-10 14:31:30 -07:00
topjohnwu
0499588107 Support androidboot.fstab_suffix cmdline flag
Fix #3187
2020-10-08 03:04:12 -07:00
topjohnwu
d4d837a562 Update docs and README 2020-10-08 01:13:00 -07:00
topjohnwu
fbcbb20178 Update app changelog 2020-10-08 01:06:37 -07:00
topjohnwu
0914700fc6 Fix string resources 2020-10-08 00:50:21 -07:00
vvb2060
eeced2fb5b Only care about the main process when not expanded 2020-10-08 00:30:34 -07:00
topjohnwu
6509e3d4f5 Use ProgressDialog when restoring images
Close #3287
2020-10-08 00:27:16 -07:00
AdiityaAndre
317052604b Translation: Update and improve Indonesian strings
* Added new strings
* Decapitalized wording
2020-10-08 00:16:29 -07:00
Wagg13
5538f7168c Update PT-BR translation 2020-10-08 00:15:56 -07:00
vvb2060
dcb9e4cd93 Update zh-rCN translation 2020-10-08 00:15:29 -07:00
topjohnwu
d9382f59bf Fix logical error 2020-10-08 00:14:51 -07:00
topjohnwu
403a0c770a Fix typo 2020-10-07 20:15:23 -07:00
topjohnwu
f0f1cdc501 Disable LoggingInterceptor 2020-10-07 04:01:50 -07:00
topjohnwu
4e272b70ef Download GitHub files through CDN 2020-10-07 04:01:03 -07:00
topjohnwu
8dc62a0232 Update docs and README 2020-10-06 05:10:19 -07:00
topjohnwu
9225b47568 Tidy up network services
Add jsdelivr CDN for several files
2020-10-06 04:58:46 -07:00
topjohnwu
d462873e74 Prevent UI loop on low memory device
Fix #3215, close #3216
2020-10-06 02:21:57 -07:00
topjohnwu
fc19b50290 Cleanup ActivityResult callbacks 2020-10-06 02:04:19 -07:00
JoanVC100
333fe6da0e Update catalan 2020-10-06 00:45:17 -07:00
Madis Otenurm
75fcda9f81 Estonian update 2020-10-06 00:43:31 -07:00
kam821
44ba2a9903 Update Polish translation
Added missing strings.
2020-10-06 00:42:48 -07:00
Fs00
2fceb1ad96 Improve accuracy and correctness of Italian translation 2020-10-06 00:41:59 -07:00
vvb2060
bacb5fa462 Truncate existing file when openOutputStream 2020-10-06 00:41:21 -07:00
topjohnwu
67f8dc494e Properly patch vbmeta.img
Close #3241
2020-10-06 00:40:57 -07:00
topjohnwu
3e4caabecb Update FAQ 2020-10-03 14:02:19 -07:00
Linus Groh
dcd5183b24 Fix two typos in v21.0 release notes 2020-10-03 04:32:08 -07:00
topjohnwu
d80c6b42a6 Update README 2020-10-03 04:28:43 -07:00
401 changed files with 21002 additions and 17175 deletions

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## READ BEFORE OPENING ISSUES
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk Manager, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
If you experience a crash of Magisk Manager, dump the full `logcat` **when the crash happens**. **DO NOT** upload `magisk.log`.
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
**DO NOT** open issues regarding root detection.
**DO NOT** ask for instructions.
**DO NOT** report issues if you have any modules installed.
Without following the rules above, your issue will be closed without explanation.

90
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,90 @@
name: Magisk Build
on:
push:
branches: [ master ]
paths:
- 'app/**'
- 'native/**'
- 'stub/**'
- 'buildSrc/**'
- 'build.py'
- 'gradle.properties'
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macOS-latest ]
steps:
- name: Check out
uses: actions/checkout@v2
with:
submodules: 'recursive'
fetch-depth: 0
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11'
- name: Set up Python 3
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Set up GitHub env (Windows)
if: runner.os == 'Windows'
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)
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
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Cache NDK
id: ndk-cache
uses: actions/cache@v2
with:
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }}
- name: Set up NDK
if: steps.ndk-cache.outputs.cache-hit != 'true'
run: python build.py ndk
- name: Build release
run: python build.py -vr all
- name: Build debug
run: python build.py -v all
# Only upload artifacts built on Linux
- name: Upload build artifact
if: runner.os == 'Linux'
uses: actions/upload-artifact@v2
with:
name: ${{ github.sha }}
path: out

3
.gitmodules vendored
View File

@@ -25,6 +25,9 @@
[submodule "pcre"]
path = native/jni/external/pcre
url = https://android.googlesource.com/platform/external/pcre
[submodule "xhook"]
path = native/jni/external/xhook
url = https://github.com/iqiyi/xHook.git
[submodule "termux-elf-cleaner"]
path = tools/termux-elf-cleaner
url = https://github.com/termux/termux-elf-cleaner.git

View File

@@ -15,17 +15,19 @@ Here are some feature highlights:
## Downloads
[![](https://img.shields.io/badge/Magisk%20Manager-v7.5.1-green)](https://github.com/topjohnwu/Magisk/releases/download/manager-v7.5.1/MagiskManager-v7.5.1.apk)
Please note that the only source of official Magisk information and downloads is [Github](https://github.com/topjohnwu/Magisk/). Beware of any other websites.
[![](https://img.shields.io/badge/Magisk%20Manager-v8.0.7-green)](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.7/MagiskManager-v8.0.7.apk)
[![](https://img.shields.io/badge/Magisk%20Manager-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
<br>
[![](https://img.shields.io/badge/Magisk-v20.4-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
[![](https://img.shields.io/badge/Magisk%20Beta-v20.4-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
[![](https://img.shields.io/badge/Magisk-v21.4-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v21.4)
[![](https://img.shields.io/badge/Magisk%20Beta-v21.4-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v21.4)
## Useful Links
- [Installation Instruction](https://topjohnwu.github.io/Magisk/install.html)
- [Frequently Asked Questions](https://topjohnwu.github.io/Magisk/faq.html)
- [Full Official Docs](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))
## Android Version Support
@@ -56,13 +58,13 @@ For Magisk Manager crashes, record and upload the logcat when the crash occurs.
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"`
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
- Set environment variable `ANDROID_HOME` 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
- Set configurations in `config.prop`. A sample `config.prop.sample` is provided.
- To start building, run `build.py` to see your options. \
For each action, use `-h` to access help (e.g. `./build.py all -h`)
- To start development, open the project in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
- `build.py` builds in debug mode by default. If you want release builds (with `-r, --release`), you need a Java Keystore to sign APKs and zips. For more information, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
- 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.
- 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).
## Translation Contributions

5
app/.gitignore vendored
View File

@@ -6,6 +6,7 @@
app/release
*.hprof
.externalNativeBuild/
public.certificate.x509.pem
private.key.pk8
*.apk
src/main/assets
src/main/jniLibs
src/main/resources

View File

@@ -1,7 +1,10 @@
import org.apache.tools.ant.filters.FixCrLfFilter
import java.io.PrintStream
plugins {
id("com.android.application")
kotlin("android")
kotlin("android.extensions")
kotlin("plugin.parcelize")
kotlin("kapt")
id("androidx.navigation.safeargs.kotlin")
}
@@ -20,12 +23,12 @@ android {
applicationId = "com.topjohnwu.magisk"
vectorDrawables.useSupportLibrary = true
multiDexEnabled = true
versionName = Config["appVersion"]
versionCode = Config["appVersionCode"]?.toInt()
buildConfigField("int", "LATEST_MAGISK", Config["versionCode"] ?: "Integer.MAX_VALUE")
versionName = Config.version
versionCode = Config.versionCode
ndk.abiFilters("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
javaCompileOptions.annotationProcessorOptions.arguments(
mapOf("room.incremental" to "true")
mapOf("room.incremental" to "true")
)
}
@@ -50,12 +53,14 @@ android {
}
packagingOptions {
exclude("/META-INF/**")
exclude("/androidsupportmultidexversion.txt")
exclude("/META-INF/*")
exclude("/org/bouncycastle/**")
exclude("/kotlin/**")
exclude("/kotlinx/**")
exclude("/okhttp3/**")
exclude("/*.txt")
exclude("/*.bin")
doNotStrip("**/*.so")
}
kotlinOptions {
@@ -63,39 +68,146 @@ android {
}
}
androidExtensions {
isExperimental = true
val syncLibs by tasks.registering(Sync::class) {
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 copyUtils = tasks.register("copyUtils", Copy::class) {
from(rootProject.file("scripts/util_functions.sh"))
into("src/main/res/raw")
val createStubLibs by tasks.registering {
dependsOn(syncLibs)
doLast {
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()
}
}
tasks["preBuild"]?.dependsOn(copyUtils)
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 {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(kotlin("stdlib"))
implementation(project(":app:shared"))
implementation(project(":app:signing"))
implementation("com.github.topjohnwu:jtar:1.0.0")
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
implementation("com.github.topjohnwu:lz4-java:1.7.1")
implementation("com.jakewharton.timber:timber:4.7.1")
val vBC = "1.68"
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}")
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}")
val vBAdapt = "4.0.0"
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
implementation("${bindingAdapter}:${vBAdapt}")
implementation("${bindingAdapter}-recyclerview:${vBAdapt}")
val vMarkwon = "4.6.0"
val vMarkwon = "4.6.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.0.2"
val vLibsu = "3.1.1"
implementation("com.github.topjohnwu.libsu:core:${vLibsu}")
implementation("com.github.topjohnwu.libsu:io:${vLibsu}")
@@ -118,11 +230,11 @@ dependencies {
implementation("com.squareup.okhttp3:logging-interceptor:${vOkHttp}")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:${vOkHttp}")
val vMoshi = "1.10.0"
val vMoshi = "1.11.0"
implementation("com.squareup.moshi:moshi:${vMoshi}")
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
val vRoom = "2.2.5"
val vRoom = "2.3.0-beta01"
implementation("androidx.room:room-runtime:${vRoom}")
implementation("androidx.room:room-ktx:${vRoom}")
kapt("androidx.room:room-compiler:${vRoom}")
@@ -131,17 +243,16 @@ dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:${vNav}")
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
implementation("androidx.biometric:biometric:1.0.1")
implementation("androidx.constraintlayout:constraintlayout:2.0.1")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.browser:browser:1.2.0")
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.2.5")
implementation("androidx.work:work-runtime-ktx:2.4.0")
implementation("androidx.transition:transition:1.3.1")
implementation("androidx.work:work-runtime-ktx:2.5.0")
implementation("androidx.transition:transition:1.4.0")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.core:core-ktx:1.3.1")
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.0.0")
implementation("com.google.android.material:material:1.2.1")
implementation("androidx.core:core-ktx:1.3.2")
implementation("com.google.android.material:material:1.3.0")
}

View File

@@ -18,16 +18,10 @@
# Kotlin
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
public static void checkExpressionValueIsNotNull(...);
public static void checkNotNullExpressionValue(...);
public static void checkReturnedValueIsNotNull(...);
public static void checkFieldIsNotNull(...);
public static void checkParameterIsNotNull(...);
public static void check*(...);
public static void throw*(...);
}
# Stubs
-keep class a.* { *; }
# Snet
-keepclassmembers class com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.ui.safetynet.SafetyNetHelper$Callback
@@ -35,20 +29,17 @@
void onResponse(org.json.JSONObject);
}
# Fragments
# TODO: Remove when AGP 4.1 release
# https://issuetracker.google.com/issues/142601969
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
-keepnames class androidx.navigation.fragment.NavHostFragment
# Stub
-keep class com.topjohnwu.magisk.core.App { <init>(java.lang.Object); }
# Strip Timber verbose and debug logging
-assumenosideeffects class timber.log.Timber.Tree {
-assumenosideeffects class timber.log.Timber$Tree {
public void v(**);
public void d(**);
}
# Excessive obfuscation
-repackageclasses
-repackageclasses 'a'
-allowaccessmodification
# QOL

View File

@@ -1,23 +1,27 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.shared">
package="com.topjohnwu.shared"
android:installLocation="internalOnly">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:label="Magisk Manager"
android:installLocation="internalOnly"
android:usesCleartextTraffic="true"
android:allowBackup="false"
android:label="Magisk"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:ignore="UnusedAttribute">
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute" />
<provider
android:name="a.p"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
</provider>
</application>
</manifest>

View File

@@ -1,7 +0,0 @@
package a;
import com.topjohnwu.magisk.FileProvider;
public class p extends FileProvider {
/* Stub */
}

View File

@@ -8,7 +8,6 @@ import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
@@ -34,8 +33,6 @@ public class FileProvider extends ContentProvider {
private PathStrategy mStrategy;
public static ProviderCallHandler callHandler;
@Override
public boolean onCreate() {
return true;
@@ -131,13 +128,6 @@ public class FileProvider extends ContentProvider {
return file.delete() ? 1 : 0;
}
@Override
public Bundle call(String method, String arg, Bundle extras) {
if (callHandler != null)
return callHandler.call(getContext(), method, arg, extras);
return Bundle.EMPTY;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {

View File

@@ -1,8 +0,0 @@
package com.topjohnwu.magisk;
import android.content.Context;
import android.os.Bundle;
public interface ProviderCallHandler {
Bundle call(Context context, String method, String arg, Bundle extras);
}

View File

@@ -1,7 +1,10 @@
package com.topjohnwu.magisk.utils;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Build;
@@ -10,20 +13,36 @@ import com.topjohnwu.magisk.FileProvider;
import java.io.File;
public class APKInstall {
public static Intent installIntent(Context c, File apk) {
Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 24) {
intent.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
} else {
//noinspection ResultOfMethodCallIgnored SetWorldReadable
apk.setReadable(true, false);
intent.setData(Uri.fromFile(apk));
}
return intent;
}
public static void install(Context c, File apk) {
c.startActivity(installIntent(c, apk));
}
public static Intent installIntent(Context c, File apk) {
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
install.setData(FileProvider.getUriForFile(c, c.getPackageName() + ".provider", apk));
} else {
apk.setReadable(true, false);
install.setData(Uri.fromFile(apk));
}
return install;
public static void registerInstallReceiver(Context c, BroadcastReceiver r) {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addAction(Intent.ACTION_PACKAGE_ADDED);
filter.addDataScheme("package");
c.getApplicationContext().registerReceiver(r, filter);
}
public static void installHideResult(Activity c, File apk) {
Intent intent = installIntent(c, apk);
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
c.startActivityForResult(intent, 0); // Ignore result, use install receiver
}
}

View File

@@ -1 +0,0 @@
/build

View File

@@ -1,35 +0,0 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
id("java-library")
id("java")
id("com.github.johnrengelman.shadow") version "6.0.0"
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
val jar by tasks.getting(Jar::class) {
manifest {
attributes["Main-Class"] = "com.topjohnwu.signing.ZipSigner"
}
}
val shadowJar by tasks.getting(ShadowJar::class) {
archiveBaseName.set("zipsigner")
archiveClassifier.set(null as String?)
archiveVersion.set("4.0")
}
repositories {
jcenter()
}
dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
api("org.bouncycastle:bcprov-jdk15on:1.66")
api("org.bouncycastle:bcpkix-jdk15on:1.66")
}

View File

@@ -1,49 +0,0 @@
package com.topjohnwu.signing;
import java.io.FileInputStream;
import java.io.InputStream;
public class BootSigner {
public static void main(String[] args) throws Exception {
if (args.length > 0 && "-verify".equals(args[0])) {
String certPath = "";
if (args.length >= 2) {
/* args[1] is the path to a public key certificate */
certPath = args[1];
}
boolean signed = SignBoot.verifySignature(System.in,
certPath.isEmpty() ? null : new FileInputStream(certPath));
System.exit(signed ? 0 : 1);
} else if (args.length > 0 && "-sign".equals(args[0])) {
InputStream cert = null;
InputStream key = null;
String name = "/boot";
if (args.length >= 3) {
cert = new FileInputStream(args[1]);
key = new FileInputStream(args[2]);
}
if (args.length == 2) {
name = args[1];
} else if (args.length >= 4) {
name = args[3];
}
boolean success = SignBoot.doSignature(name, System.in, System.out, cert, key);
System.exit(success ? 0 : 1);
} else {
System.err.println(
"BootSigner <actions> [args]\n" +
"Input from stdin, outputs to stdout\n" +
"\n" +
"Actions:\n" +
" -verify [x509.pem]\n" +
" verify image, cert is optional\n" +
" -sign [x509.pem] [pk8] [name]\n" +
" sign image, name, cert and key pair are optional\n" +
" name should be /boot (default) or /recovery\n"
);
}
}
}

View File

@@ -1,81 +0,0 @@
package com.topjohnwu.signing;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class ZipSigner {
private static void usage() {
System.err.println("ZipSigner usage:");
System.err.println(" zipsigner.jar input.jar output.jar");
System.err.println(" sign jar with AOSP test keys");
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
System.err.println(" sign jar with certificate / private key pair");
System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar");
System.err.println(" sign jar with Java KeyStore");
System.exit(2);
}
private static void sign(JarMap input, FileOutputStream output) throws Exception {
sign(SignApk.class.getResourceAsStream("/keys/testkey.x509.pem"),
SignApk.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
}
private static void sign(InputStream certIs, InputStream keyIs,
JarMap input, FileOutputStream output) throws Exception {
X509Certificate cert = CryptoUtils.readCertificate(certIs);
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
SignApk.sign(cert, key, input, output);
}
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
JarMap in, FileOutputStream out) throws Exception {
KeyStore ks;
try {
ks = KeyStore.getInstance("JKS");
try (InputStream is = new FileInputStream(keyStore)) {
ks.load(is, keyStorePass.toCharArray());
}
} catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) {
ks = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream(keyStore)) {
ks.load(is, keyStorePass.toCharArray());
}
}
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
SignApk.sign(cert, key, in, out);
}
public static void main(String[] args) throws Exception {
if (args.length != 2 && args.length != 4 && args.length != 6)
usage();
Security.insertProviderAt(new BouncyCastleProvider(), 1);
try (JarMap in = JarMap.open(args[args.length - 2], false);
FileOutputStream out = new FileOutputStream(args[args.length - 1])) {
if (args.length == 2) {
sign(in, out);
} else if (args.length == 4) {
try (InputStream cert = new FileInputStream(args[0]);
InputStream key = new FileInputStream(args[1])) {
sign(cert, key, in, out);
}
} else if (args.length == 6) {
sign(args[0], args[1], args[2], args[3], in, out);
}
}
}
}

View File

@@ -3,22 +3,18 @@
xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.magisk">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<application
android:name=".core.App"
android:extractNativeLibs="true"
android:icon="@drawable/ic_launcher"
android:name="a.e"
android:allowBackup="true"
android:multiArch="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
<!-- Splash -->
<activity
android:name="a.c"
android:name=".core.SplashActivity"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -31,27 +27,27 @@
</activity>
<!-- Main -->
<activity android:name="a.b" />
<activity android:name=".ui.MainActivity" />
<!-- Superuser -->
<activity
android:name="a.m"
android:name=".ui.surequest.SuRequestActivity"
android:directBootAware="true"
android:excludeFromRecents="true"
android:exported="false"
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Receiver -->
<receiver
android:name="a.h"
android:directBootAware="true">
android:name=".core.Receiver"
android:directBootAware="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<intent-filter>
@@ -63,7 +59,15 @@
</receiver>
<!-- DownloadService -->
<service android:name="a.j" />
<service android:name=".core.download.DownloadService" />
<!-- FileProvider -->
<provider
android:name=".core.Provider"
android:authorities="${applicationId}.provider"
android:directBootAware="true"
android:exported="false"
android:grantUriPermissions="true" />
<!-- Hardcode GMS version -->
<meta-data
@@ -74,16 +78,12 @@
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
tools:node="remove"
tools:ignore="ExportedContentProvider" />
<!-- We don't invalidate Room -->
<service
android:name="androidx.room.MultiInstanceInvalidationService"
tools:node="remove"/>
<!-- We don't use Device Credentials -->
<activity
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
tools:node="remove" />
</application>

View File

@@ -1,29 +0,0 @@
@file:JvmName("a")
package a
import com.topjohnwu.magisk.core.App
import com.topjohnwu.magisk.core.GeneralReceiver
import com.topjohnwu.magisk.core.SplashActivity
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.signing.BootSigner
fun main(args: Array<String>) {
BootSigner.main(args)
}
class b : MainActivity()
class c : SplashActivity()
class e : App {
constructor() : super()
constructor(o: Any) : super(o)
}
class h : GeneralReceiver()
class j : DownloadService()
class m : SuRequestActivity()

View File

@@ -1,6 +1,8 @@
package com.topjohnwu.magisk.arch
import android.content.Intent
import android.content.res.Resources
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.KeyEvent
import android.view.View
@@ -15,6 +17,7 @@ import androidx.navigation.fragment.NavHostFragment
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory
import com.topjohnwu.magisk.ui.theme.Theme
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
@@ -41,12 +44,9 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
AppCompatDelegate.setDefaultNightMode(theme)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
currentFragment?.onActivityResult(requestCode, resultCode, data)
}
override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
setTheme(themeRes)
super.onCreate(savedInstanceState)
@@ -65,6 +65,31 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
directionsDispatcher.value = null
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window?.decorView?.let {
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) {
window?.decorView?.post {
// If navigation bar is short enough (gesture navigation enabled), make it transparent
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
window.isStatusBarContrastEnforced = false
}
}
}
}
}
}
fun setContentView() {
@@ -72,8 +97,10 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this
}
}
ensureInsets()
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
viewRoot.rootView.accessibilityDelegate = delegate
}
override fun onResume() {
@@ -85,7 +112,7 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
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 ActivityExecutor -> event(this)
else -> Unit

View File

@@ -1,12 +1,9 @@
package com.topjohnwu.magisk.arch
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.LifecycleOwner
interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
val viewRoot: View
val viewModel: VM
@@ -17,47 +14,8 @@ interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
}
}
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
/**
* Called for all [ViewEvent]s published by associated viewModel.
*/
fun onEventDispatched(event: ViewEvent) {}
fun ensureInsets() {
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
insets.asInsets()
.also { viewModel.insets = it }
.let { consumeSystemWindowInsets(it) }
?.subtractBy(insets) ?: insets
}
if (ViewCompat.isAttachedToWindow(viewRoot)) {
ViewCompat.requestApplyInsets(viewRoot)
} else {
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewDetachedFromWindow(v: View) = Unit
override fun onViewAttachedToWindow(v: View) {
ViewCompat.requestApplyInsets(v)
}
})
}
}
private fun WindowInsetsCompat.asInsets() = Insets.of(
systemWindowInsetLeft,
systemWindowInsetTop,
systemWindowInsetRight,
systemWindowInsetBottom
)
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
Insets.of(
insets.systemWindowInsetLeft - left,
insets.systemWindowInsetTop - top,
insets.systemWindowInsetRight - right,
insets.systemWindowInsetBottom - bottom
)
).build()
}

View File

@@ -17,15 +17,13 @@ import com.topjohnwu.magisk.ktx.startAnimations
abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
Fragment(), BaseUIComponent<VM> {
protected val activity get() = requireActivity() as BaseUIActivity<*, *>
val activity get() = requireActivity() as BaseUIActivity<*, *>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
override val viewRoot: View get() = binding.root
private val navigation get() = activity.navigation
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startObserveEvents()
@@ -43,6 +41,11 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
return binding.root
}
override fun onStart() {
super.onStart()
activity.supportActionBar?.subtitle = null
}
override fun onEventDispatched(event: ViewEvent) = when(event) {
is ContextExecutor -> event(requireContext())
is ActivityExecutor -> event(activity)
@@ -65,7 +68,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
return true
}
})
ensureInsets()
}
override fun onResume() {

View File

@@ -9,18 +9,18 @@ import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.OnItemBind
inline fun <T : ComparableRvItem<*>> diffListOf(
fun <T : ComparableRvItem<*>> diffListOf(
vararg newItems: T
) = diffListOf(newItems.toList())
inline fun <T : ComparableRvItem<*>> diffListOf(
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) }
inline fun <T : ComparableRvItem<*>> filterableListOf(
fun <T : ComparableRvItem<*>> filterableListOf(
vararg newItems: T
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)

View File

@@ -1,7 +1,6 @@
package com.topjohnwu.magisk.arch
import android.content.Context
import androidx.fragment.app.Fragment
import kotlinx.coroutines.CoroutineScope
/**
@@ -23,5 +22,5 @@ interface ActivityExecutor {
}
interface FragmentExecutor {
operator fun invoke(fragment: Fragment)
operator fun invoke(fragment: BaseUIFragment<*, *>)
}

View File

@@ -10,10 +10,9 @@ import androidx.multidex.MultiDex
import androidx.work.WorkManager
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.utils.AppShellInit
import com.topjohnwu.magisk.core.utils.BusyBoxInit
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
import com.topjohnwu.magisk.core.utils.RootInit
import com.topjohnwu.magisk.core.utils.updateConfig
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.ktx.unwrap
@@ -21,6 +20,7 @@ import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
import java.io.File
import kotlin.system.exitProcess
open class App() : Application() {
@@ -33,10 +33,9 @@ open class App() : Application() {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
Shell.setDefaultBuilder(Shell.Builder.create()
.setFlags(Shell.FLAG_MOUNT_MASTER)
.setInitializers(RootInit::class.java)
.setInitializers(BusyBoxInit::class.java, AppShellInit::class.java)
.setTimeout(2))
Shell.EXECUTOR = IODispatcherExecutor()
FileProvider.callHandler = SuCallbackHandler
// Always log full stack trace with Timber
Timber.plant(Timber.DebugTree())
@@ -64,12 +63,18 @@ open class App() : Application() {
val wrapped = impl.wrap()
super.attachBaseContext(wrapped)
val info = base.applicationInfo
val libDir = runCatching {
info.javaClass.getDeclaredField("secondaryNativeLibraryDir").get(info) as String?
}.getOrNull() ?: info.nativeLibraryDir
Const.NATIVE_LIB_DIR = File(libDir)
// Normal startup
startKoin {
androidContext(wrapped)
modules(koinModules)
}
ResMgr.init(impl)
AssetHack.init(impl)
app.registerActivityLifecycleCallbacks(ForegroundTracker)
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
}

View File

@@ -1,5 +1,6 @@
package com.topjohnwu.magisk.core
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.util.Xml
@@ -9,19 +10,15 @@ import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
import com.topjohnwu.magisk.core.magiskdb.StringDao
import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.defaultLocale
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.data.preference.PreferenceModel
import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ui.theme.Theme
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import org.xmlpull.v1.XmlPullParser
import java.io.File
import java.io.InputStream
object Config : PreferenceModel, DBConfig {
@@ -29,6 +26,15 @@ object Config : PreferenceModel, DBConfig {
override val settingsDao: SettingsDao by inject()
override val context: Context by inject(Protected)
@get:SuppressLint("ApplySharedPref")
val prefsFile: File get() {
// Flush prefs to disk
prefs.edit().apply {
remove(Key.ASKED_HOME)
}.commit()
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
}
object Key {
// db configs
const val ROOT_ACCESS = "root_access"
@@ -43,6 +49,7 @@ object Config : PreferenceModel, DBConfig {
const val SU_AUTO_RESPONSE = "su_auto_response"
const val SU_NOTIFICATION = "su_notification"
const val SU_REAUTH = "su_reauth"
const val SU_TAPJACK = "su_tapjack"
const val CHECK_UPDATES = "check_update"
const val UPDATE_CHANNEL = "update_channel"
const val CUSTOM_CHANNEL = "custom_channel"
@@ -119,7 +126,7 @@ object Config : PreferenceModel, DBConfig {
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suAutoResponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
@@ -127,8 +134,9 @@ object Config : PreferenceModel, DBConfig {
var darkTheme by preference(Key.DARK_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
var suReAuth by preference(Key.SU_REAUTH, false)
var suTapjack by preference(Key.SU_TAPJACK, true)
var checkUpdate by preference(Key.CHECK_UPDATES, true)
var doh by preference(Key.DOH, defaultLocale.country == "CN")
var doh by preference(Key.DOH, false)
var magiskHide by preference(Key.MAGISKHIDE, true)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
@@ -150,8 +158,13 @@ object Config : PreferenceModel, DBConfig {
private const val SU_FINGERPRINT = "su_fingerprint"
fun initialize() {
prefs.edit { parsePrefs() }
fun load(pkg: String?) {
// Only try to load prefs when fresh install and a previous package name is set
if (pkg != null && prefs.all.isEmpty()) runCatching {
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
prefs.edit { parsePrefs(it) }
}
}
prefs.edit {
// Settings migration
@@ -173,10 +186,8 @@ object Config : PreferenceModel, DBConfig {
}
}
private fun SharedPreferences.Editor.parsePrefs() {
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
if (config.exists()) runCatching {
val input = SuFileInputStream(config)
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
runCatching {
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, "UTF-8")
@@ -220,21 +231,6 @@ object Config : PreferenceModel, DBConfig {
else -> parser.next()
}
}
config.delete()
}
}
fun export() {
// Flush prefs to disk
prefs.edit().apply {
remove(Key.ASKED_HOME)
}.commit()
val context = get<Context>(Protected)
val xml = File(
"${context.filesDir.parent}/shared_prefs",
"${context.packageName}_preferences.xml"
)
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
}
}

View File

@@ -1,67 +1,76 @@
package com.topjohnwu.magisk.core
import android.os.Build
import android.os.Process
import com.topjohnwu.magisk.BuildConfig
import java.io.File
@Suppress("DEPRECATION")
object Const {
val CPU_ABI: String
val CPU_ABI_32: String
init {
if (Build.VERSION.SDK_INT >= 21) {
CPU_ABI = Build.SUPPORTED_ABIS[0]
CPU_ABI_32 = Build.SUPPORTED_32_BIT_ABIS.firstOrNull() ?: CPU_ABI
} else {
CPU_ABI = Build.CPU_ABI
CPU_ABI_32 = CPU_ABI
}
}
// Paths
lateinit var MAGISKTMP: String
lateinit var NATIVE_LIB_DIR: File
val MAGISK_PATH get() = "$MAGISKTMP/modules"
const val TMP_FOLDER_PATH = "/dev/tmp"
const val TMPDIR = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
// Versions
const val SNET_EXT_VER = 15
const val SNET_REVISION = "d494bc726e86166913a13629e3b1336728ec5d7f"
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
const val SNET_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
// Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml"
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
val USER_ID = Process.myUid() / 100000
object Version {
const val MIN_VERSION = "v19.0"
const val MIN_VERCODE = 19000
const val MIN_VERSION = "v20.4"
const val MIN_VERCODE = 20400
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary()
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
}
object ID {
const val FETCH_ZIP = 2
const val SELECT_BOOT = 3
// notifications
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
const val APK_UPDATE_NOTIFICATION_ID = 5
const val DTBO_NOTIFICATION_ID = 7
const val HIDE_MANAGER_NOTIFICATION_ID = 8
const val UPDATE_NOTIFICATION_CHANNEL = "update"
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
}
object Url {
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
val CHANGELOG_URL = if (BuildConfig.VERSION_CODE % 100 != 0) Info.remote.magisk.note
else "https://topjohnwu.github.io/Magisk/releases/${BuildConfig.VERSION_CODE}.md"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
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 {
// others
const val LINK_KEY = "Link"
const val IF_NONE_MATCH = "If-None-Match"
const val ETAG_KEY = "ETag"
// intents
const val OPEN_SECTION = "section"
const val PREV_PKG = "prev_pkg"
}
object Value {

View File

@@ -14,44 +14,38 @@ import android.content.Intent
import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import android.util.DisplayMetrics
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.utils.refreshLocale
import com.topjohnwu.magisk.core.utils.updateConfig
import com.topjohnwu.magisk.ktx.forceGetDeclaredField
import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
fun AssetManager.addAssetPath(path: String) {
DynAPK.addAssetPath(this, path)
}
fun Context.wrap(global: Boolean = true): Context =
if (global) GlobalResContext(this) else ResContext(this)
fun Context.wrap(inject: Boolean = false): Context =
if (inject) ReInjectedContext(this) else InjectedContext(this)
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
fun Context.wrapJob(): Context = object : InjectedContext(this) {
override fun getApplicationContext(): Context {
return this
}
override fun getApplicationContext() = this
@SuppressLint("NewApi")
override fun getSystemService(name: String): Any? {
return if (!isRunningAsStub) super.getSystemService(name) else
when (name) {
Context.JOB_SCHEDULER_SERVICE ->
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
else -> super.getSystemService(name)
return super.getSystemService(name).let {
when {
!isRunningAsStub -> it
name == JOB_SCHEDULER_SERVICE -> JobSchedulerWrapper(it as JobScheduler)
else -> it
}
}
}
}
fun Class<*>.cmp(pkg: String): ComponentName {
val name = ClassMap[this].name
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
}
fun Class<*>.cmp(pkg: String) =
ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
inline fun <reified T> Activity.redirect() = Intent(intent)
.setComponent(T::class.java.cmp(packageName))
@@ -59,34 +53,27 @@ inline fun <reified T> Activity.redirect() = Intent(intent)
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
open val mRes: Resources get() = ResMgr.resource
override fun getResources(): Resources {
return mRes
}
override fun getClassLoader(): ClassLoader {
return javaClass.classLoader!!
}
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 ResContext(super.createConfigurationContext(config))
return super.createConfigurationContext(config).wrap(true)
}
}
private class ResContext(base: Context) : GlobalResContext(base) {
override val mRes by lazy { base.resources.patch() }
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(ResMgr.apk)
assets.addAssetPath(AssetHack.apk)
return this
}
}
object ResMgr {
object AssetHack {
lateinit var resource: Resources
lateinit var apk: String
@@ -101,62 +88,39 @@ object ResMgr {
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): Int {
return base.schedule(job.patch())
}
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
return base.enqueue(job.patch(), work)
}
override fun cancel(jobId: Int) {
base.cancel(jobId)
}
override fun cancelAll() {
base.cancelAll()
}
override fun getAllPendingJobs(): List<JobInfo> {
return base.allPendingJobs
}
override fun getPendingJob(jobId: Int): JobInfo? {
return base.getPendingJob(jobId)
}
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 {
// We need to swap out the service of JobInfo
val name = service.className
val component = ComponentName(
service.packageName,
Info.stubChk.classToComponent[name] ?: name
)
// 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)
javaClass.forceGetDeclaredField("service")?.set(this, component)
return this
}
}
private object ClassMap {
private val map = mapOf(
App::class.java to a.e::class.java,
MainActivity::class.java to a.b::class.java,
SplashActivity::class.java to a.c::class.java,
GeneralReceiver::class.java to a.h::class.java,
DownloadService::class.java to a.j::class.java,
SuRequestActivity::class.java to a.m::class.java
)
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
}
// Keep a reference to these resources to prevent it from
// being removed when running "remove unused resources"
val shouldKeepResources = listOf(

View File

@@ -1,41 +1,45 @@
package com.topjohnwu.magisk.core
import android.os.Build
import androidx.databinding.ObservableBoolean
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.core.utils.net.NetworkObserver
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.utils.CachedValue
import com.topjohnwu.magisk.ktx.getProperty
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils.fastCmd
import com.topjohnwu.superuser.internal.UiThreadHandler
import java.io.FileInputStream
import java.io.File
import java.io.IOException
import java.util.*
val isRunningAsStub get() = Info.stub != null
object Info {
val envRef = CachedValue { loadState() }
@JvmStatic val env by envRef
var stub: DynAPK.Data? = null
val stubChk: DynAPK.Data
get() = stub as DynAPK.Data
var remote = UpdateInfo()
val EMPTY_REMOTE = UpdateInfo()
var remote = EMPTY_REMOTE
suspend fun getRemote(svc: NetworkService): UpdateInfo? {
return if (remote === EMPTY_REMOTE) {
svc.fetchUpdate()?.apply { remote = this }
} else remote
}
// Device state
var crypto = ""
@JvmStatic var isSAR = false
@JvmStatic var isAB = false
@JvmStatic val env by lazy { loadState() }
@JvmField var isSAR = false
@JvmField var isAB = false
@JvmField val isVirtualAB = getProperty("ro.virtual_ab.enabled", "false") == "true"
@JvmStatic val isFDE get() = crypto == "block"
@JvmStatic var ramdisk = false
@JvmStatic var hasGMS = true
@JvmStatic var isPixel = false
@JvmStatic val cryptoText get() = crypto.capitalize(Locale.US)
@JvmField var ramdisk = false
@JvmField var hasGMS = true
@JvmField val isPixel = Build.BRAND == "google"
@JvmField val isEmulator = getProperty("ro.kernel.qemu", "0") == "1"
var crypto = ""
var noDataExec = false
val isConnected by lazy {
ObservableBoolean(false).also { field ->
@@ -47,14 +51,12 @@ object Info {
val isNewReboot by lazy {
try {
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
val id = it.readLine()
if (id != Config.bootId) {
Config.bootId = id
true
} else {
false
}
val id = File("/proc/sys/kernel/random/boot_id").readText()
if (id != Config.bootId) {
Config.bootId = id
true
} else {
false
}
} catch (e: IOException) {
false
@@ -73,8 +75,8 @@ object Info {
hide: Boolean = false
) {
val magiskHide get() = Config.magiskHide
val magiskVersionCode = when (code) {
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
val magiskVersionCode = when {
code < Const.Version.MIN_VERCODE -> -1
else -> if (Shell.rootAccess()) code else -1
}
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE

View File

@@ -0,0 +1,39 @@
package com.topjohnwu.magisk.core
import android.content.Context
import android.content.pm.ProviderInfo
import android.net.Uri
import android.os.Bundle
import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import java.io.File
open class Provider : FileProvider() {
override fun attachInfo(context: Context, info: ProviderInfo?) {
super.attachInfo(context.wrap(), info)
}
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
SuCallbackHandler(context!!, method, extras)
return Bundle.EMPTY
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
return when (uri.encodedPath ?: return null) {
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
else -> super.openFile(uri, mode)
}
}
companion object {
fun APK_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
fun PREFS_URI(pkg: String) =
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
}
}

View File

@@ -11,7 +11,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.core.inject
open class GeneralReceiver : BaseReceiver() {
open class Receiver : BaseReceiver() {
private val policyDB: PolicyDao by inject()

View File

@@ -1,68 +1,91 @@
package com.topjohnwu.magisk.core
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.util.concurrent.CountDownLatch
open class SplashActivity : Activity() {
open class SplashActivity : BaseActivity() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
private val latch = CountDownLatch(1)
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.SplashTheme)
super.onCreate(savedInstanceState)
GlobalScope.launch(Dispatchers.IO) {
initAndStart()
}
// Pre-initialize root shell
Shell.getShell(null) { initAndStart() }
}
private fun handleRepackage() {
val pkg = Config.suManager
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
Config.suManager = ""
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
}
if (pkg == packageName) {
private fun handleRepackage(pkg: String?) {
if (packageName != APPLICATION_ID) {
runCatching {
// We are the manager, remove com.topjohnwu.magisk as it could be malware
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
// 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() {
// Pre-initialize root shell
Shell.getShell()
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
}
Config.initialize()
handleRepackage()
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 stuffs
get<GithubRawServices>()
// Pre-fetch network services
get<NetworkService>()
DONE = true
redirect<MainActivity>().also { startActivity(it) }
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,13 +1,11 @@
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.data.repository.MagiskRepository
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
import org.koin.core.inject
import java.util.concurrent.TimeUnit
@@ -15,23 +13,18 @@ import java.util.concurrent.TimeUnit
class UpdateCheckService(context: Context, workerParams: WorkerParameters)
: CoroutineWorker(context, workerParams), KoinComponent {
private val magiskRepo: MagiskRepository by inject()
private val svc: NetworkService by inject()
override suspend fun doWork(): Result {
// Make sure shell initializer was ran
withContext(Dispatchers.IO) {
Shell.getShell()
}
return magiskRepo.fetchUpdate()?.let {
if (BuildConfig.VERSION_CODE < it.app.versionCode)
return svc.fetchUpdate()?.run {
if (Info.env.isActive && BuildConfig.VERSION_CODE < magisk.versionCode)
Notifications.managerUpdate(applicationContext)
else if (Info.env.isActive && Info.env.magiskVersionCode < it.magisk.versionCode)
Notifications.magiskUpdate(applicationContext)
Result.success()
} ?: Result.failure()
}
companion object {
@SuppressLint("NewApi")
fun schedule(context: Context) {
if (Config.checkUpdate) {
val constraints = Constraints.Builder()

View File

@@ -1,6 +1,6 @@
package com.topjohnwu.magisk.core.base
import android.Manifest
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
@@ -8,6 +8,7 @@ import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Build
import android.widget.Toast
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
import androidx.collection.SparseArrayCompat
import androidx.core.app.ActivityCompat
@@ -19,11 +20,18 @@ import com.topjohnwu.magisk.ktx.set
import com.topjohnwu.magisk.utils.Utils
import kotlin.random.Random
typealias RequestCallback = BaseActivity.(Int, Intent?) -> Unit
typealias ActivityResultCallback = BaseActivity.(Int, Intent?) -> Unit
abstract class BaseActivity : AppCompatActivity() {
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
private val resultCallbacks by lazy { SparseArrayCompat<ActivityResultCallback>() }
private val newRequestCode: Int get() {
var requestCode: Int
do {
requestCode = Random.nextInt(0, 1 shl 15)
} while (resultCallbacks.containsKey(requestCode))
return requestCode
}
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
@@ -32,15 +40,14 @@ abstract class BaseActivity : AppCompatActivity() {
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap(false))
super.attachBaseContext(base.wrap(true))
}
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build()
if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE &&
Build.VERSION.SDK_INT >= 29) {
// We do not need external rw on 29+
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
// We do not need external rw on 30+
request.onSuccess()
return
}
@@ -48,8 +55,8 @@ abstract class BaseActivity : AppCompatActivity() {
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
request.onSuccess()
} else {
val requestCode = Random.nextInt(256, 512)
resultCallbacks[requestCode] = { result, _ ->
val requestCode = newRequestCode
resultCallbacks[requestCode] = { result, _ ->
if (result > 0)
request.onSuccess()
else
@@ -60,7 +67,7 @@ abstract class BaseActivity : AppCompatActivity() {
}
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
withPermission(WRITE_EXTERNAL_STORAGE, builder = builder)
}
override fun onRequestPermissionsResult(
@@ -79,16 +86,18 @@ abstract class BaseActivity : AppCompatActivity() {
}
@CallSuper
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.also {
resultCallbacks[requestCode]?.also { callback ->
resultCallbacks.remove(requestCode)
it(this, resultCode, data)
callback(this, resultCode, data)
}
}
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
resultCallbacks[requestCode] = listener
fun startActivityForResult(intent: Intent, callback: ActivityResultCallback) {
val requestCode = newRequestCode
resultCallbacks[requestCode] = callback
try {
startActivityForResult(intent, requestCode)
} catch (e: ActivityNotFoundException) {

View File

@@ -1,40 +0,0 @@
package com.topjohnwu.magisk.core.download
import android.net.Uri
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
sealed class Action : Parcelable {
sealed class Flash : Action() {
@Parcelize
object Primary : Flash()
@Parcelize
object Secondary : Flash()
}
sealed class APK : Action() {
@Parcelize
object Upgrade : APK()
@Parcelize
object Restore : APK()
}
@Parcelize
object Download : Action()
@Parcelize
object Uninstall : Action()
@Parcelize
object EnvFix : Action()
@Parcelize
data class Patch(val fileUri: Uri) : Action()
}

View File

@@ -8,11 +8,8 @@ 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.MediaStoreUtils.checkSum
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.core.utils.ProgressInputStream
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.view.Notifications
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -34,7 +31,7 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
private val notifications = Collections.synchronizedMap(HashMap<Int, Notification.Builder>())
private val coroutineScope = CoroutineScope(Dispatchers.IO)
val service: GithubRawServices by inject()
val service: NetworkService by inject()
// -- Service overrides
@@ -69,18 +66,11 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
// -- Download logic
private suspend fun Subject.startDownload() {
val skip = this is Subject.Magisk && file.checkSum("MD5", magisk.md5)
if (!skip) {
val stream = service.fetchFile(url).toProgressStream(this)
when (this) {
is Subject.Module -> // Download and process on-the-fly
stream.toModule(file, service.fetchInstaller().byteStream())
else -> {
withStreams(stream, file.outputStream()) { it, out -> it.copyTo(out) }
if (this is Subject.Manager)
handleAPK(this)
}
}
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)
@@ -117,7 +107,7 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
fun Subject.notifyID() = hashCode()
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(-1f, subject)
broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)

View File

@@ -7,11 +7,10 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.download.Action.*
import com.topjohnwu.magisk.core.download.Action.Flash.Secondary
import com.topjohnwu.magisk.core.download.Subject.*
import com.topjohnwu.magisk.core.download.Action.Flash
import com.topjohnwu.magisk.core.download.Subject.Manager
import com.topjohnwu.magisk.core.download.Subject.Module
import com.topjohnwu.magisk.core.intent
import com.topjohnwu.magisk.core.tasks.EnvFixTask
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.APKInstall
import kotlin.random.Random.Default.nextInt
@@ -22,25 +21,12 @@ open class DownloadService : BaseDownloader() {
private val context get() = this
override suspend fun onFinish(subject: Subject, id: Int) = when (subject) {
is Magisk -> subject.onFinish(id)
is Module -> subject.onFinish(id)
is Manager -> subject.onFinish(id)
}
private suspend fun Magisk.onFinish(id: Int) = when (val action = action) {
Uninstall -> FlashFragment.uninstall(file, id)
EnvFix -> {
remove(id)
EnvFixTask(file).exec()
Unit
}
is Patch -> FlashFragment.patch(file, action.fileUri, id)
is Flash -> FlashFragment.flash(file, action is Secondary, id)
else -> Unit
}
private fun Module.onFinish(id: Int) = when (action) {
is Flash -> FlashFragment.install(file, id)
Flash -> FlashFragment.install(file, id)
else -> Unit
}
@@ -53,30 +39,18 @@ open class DownloadService : BaseDownloader() {
override fun Notification.Builder.setIntent(subject: Subject)
= when (subject) {
is Magisk -> setIntent(subject)
is Module -> setIntent(subject)
is Manager -> setIntent(subject)
}
private fun Notification.Builder.setIntent(subject: Magisk)
= when (val action = subject.action) {
Uninstall -> setContentIntent(FlashFragment.uninstallIntent(context, subject.file))
is Flash -> setContentIntent(FlashFragment.flashIntent(context, subject.file, action is Secondary))
is Patch -> setContentIntent(FlashFragment.patchIntent(context, subject.file, action.fileUri))
else -> setContentIntent(Intent())
}
private fun Notification.Builder.setIntent(subject: Module)
= when (subject.action) {
is Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
Flash -> setContentIntent(FlashFragment.installIntent(context, subject.file))
else -> setContentIntent(Intent())
}
private fun Notification.Builder.setIntent(subject: Manager)
= when (subject.action) {
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
else -> setContentIntent(Intent())
}
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
private fun Notification.Builder.setContentIntent(intent: Intent) =
setContentIntent(

View File

@@ -2,23 +2,22 @@ package com.topjohnwu.magisk.core.download
import android.content.Context
import androidx.core.net.toFile
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action.APK.Restore
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.PatchAPK
import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.relaunchApp
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import java.io.File
import java.io.InputStream
import java.io.OutputStream
private fun Context.patch(apk: File) {
val patched = File(apk.parent, "patched.apk")
PatchAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
HideAPK.patch(this, apk, patched, packageName, applicationInfo.nonLocalizedLabel)
apk.delete()
patched.renameTo(apk)
}
@@ -26,47 +25,51 @@ private fun Context.patch(apk: File) {
private fun BaseDownloader.notifyHide(id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_manager_title))
.setContentTitle(getString(R.string.hide_app_title))
.setContentText("")
}
}
private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
val apk = subject.file.toFile()
val id = subject.notifyID()
private class DupOutputStream(
private val o1: OutputStream,
private val o2: OutputStream
) : OutputStream() {
override fun write(b: Int) {
o1.write(b)
o2.write(b)
}
override fun write(b: ByteArray?, off: Int, len: Int) {
o1.write(b, off, len)
o2.write(b, off, len)
}
override fun close() {
o1.close()
o2.close()
}
}
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager, stream: InputStream) {
fun write(output: OutputStream) {
val ext = subject.externalFile.outputStream()
val o = DupOutputStream(ext, output)
withStreams(stream, o) { src, out -> src.copyTo(out) }
}
if (isRunningAsStub) {
// Move to upgrade location
apk.copyTo(DynAPK.update(this), overwrite = true)
apk.delete()
if (Info.stubChk.version < subject.stub.versionCode) {
notifyHide(id)
val apk = subject.file.toFile()
val id = subject.notifyID()
write(DynAPK.update(this).outputStream())
if (Info.stub!!.version < subject.stub.versionCode) {
// Also upgrade stub
service.fetchFile(subject.stub.link).byteStream().use { it.writeTo(apk) }
notifyHide(id)
service.fetchFile(subject.stub.link).byteStream().writeTo(apk)
patch(apk)
} else {
// Simply relaunch the app
stopSelf()
relaunchApp(this)
}
} else if (packageName != BuildConfig.APPLICATION_ID) {
notifyHide(id)
patch(apk)
} else {
write(subject.file.outputStream())
}
}
private fun BaseDownloader.restore(apk: File, id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setProgress(0, 0, true)
.setContentTitle(getString(R.string.restore_img_msg))
.setContentText("")
}
Config.export()
Shell.su("pm install $apk && pm uninstall $packageName").exec()
}
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) =
when (subject.action) {
is Upgrade -> upgrade(subject)
is Restore -> restore(subject.file.toFile(), subject.notifyID())
}

View File

@@ -1,8 +1,9 @@
package com.topjohnwu.magisk.core.download
import android.net.Uri
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.ktx.forEach
import com.topjohnwu.magisk.ktx.withStreams
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -25,8 +26,7 @@ fun InputStream.toModule(file: Uri, installer: InputStream) {
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
var off = -1
var entry: ZipEntry? = zin.nextEntry
while (entry != null) {
zin.forEach { entry ->
if (off < 0) {
off = entry.name.indexOf('/') + 1
}
@@ -38,8 +38,6 @@ fun InputStream.toModule(file: Uri, installer: InputStream) {
zin.copyTo(zout)
}
}
entry = zin.nextEntry
}
}
}

View File

@@ -6,14 +6,13 @@ import android.os.Parcelable
import androidx.core.net.toUri
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.StubJson
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.get
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
@@ -26,10 +25,10 @@ sealed class Subject : Parcelable {
@Parcelize
class Module(
val module: Repo,
val module: OnlineModule,
override val action: Action
) : Subject() {
override val url: String get() = module.zipUrl
override val url: String get() = module.zip_url
override val title: String get() = module.downloadFilename
@IgnoredOnParcel
@@ -40,75 +39,26 @@ sealed class Subject : Parcelable {
@Parcelize
class Manager(
override val action: Action.APK,
private val app: ManagerJson = Info.remote.app,
private val json: MagiskJson = Info.remote.magisk,
val stub: StubJson = Info.remote.stub
) : Subject() {
override val title: String
get() = "MagiskManager-${app.version}(${app.versionCode})"
override val url: String
get() = app.link
override val action get() = Action.Download
override val title: String get() = "Magisk-${json.version}(${json.versionCode})"
override val url: String get() = json.link
@IgnoredOnParcel
override val file by lazy {
cachedFile("manager.apk")
}
val externalFile get() = MediaStoreUtils.getFile("$title.apk").uri
}
abstract class Magisk : Subject() {
val magisk: MagiskJson = Info.remote.magisk
@Parcelize
private class Internal(
override val action: Action
) : Magisk() {
override val url: String get() = magisk.link
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode})"
@IgnoredOnParcel
override val file by lazy {
cachedFile("magisk.zip")
}
}
@Parcelize
private class Uninstall : Magisk() {
override val action get() = Action.Uninstall
override val url: String get() = Info.remote.uninstaller.link
override val title: String get() = "uninstall.zip"
@IgnoredOnParcel
override val file by lazy {
cachedFile(title)
}
}
@Parcelize
private class Download : Magisk() {
override val action get() = Action.Download
override val url: String get() = magisk.link
override val title: String get() = "Magisk-${magisk.version}(${magisk.versionCode}).zip"
@IgnoredOnParcel
override val file by lazy {
MediaStoreUtils.getFile(title).uri
}
}
companion object {
operator fun invoke(config: Action) = when (config) {
Action.Download -> Download()
Action.Uninstall -> Uninstall()
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
else -> throw IllegalArgumentException()
}
}
}
}
sealed class Action : Parcelable {
@Parcelize
object Flash : Action()
@Parcelize
object Download : Action()
}

View File

@@ -2,21 +2,15 @@ package com.topjohnwu.magisk.core.model
import android.os.Parcelable
import com.squareup.moshi.JsonClass
import kotlinx.android.parcel.Parcelize
import kotlinx.parcelize.Parcelize
@JsonClass(generateAdapter = true)
data class UpdateInfo(
val app: ManagerJson = ManagerJson(),
val uninstaller: UninstallerJson = UninstallerJson(),
val magisk: MagiskJson = MagiskJson(),
val stub: StubJson = StubJson()
)
@JsonClass(generateAdapter = true)
data class UninstallerJson(
val link: String = ""
)
@Parcelize
@JsonClass(generateAdapter = true)
data class MagiskJson(
val version: String = "",
@@ -24,15 +18,6 @@ data class MagiskJson(
val link: String = "",
val note: String = "",
val md5: String = ""
)
@Parcelize
@JsonClass(generateAdapter = true)
data class ManagerJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = ""
) : Parcelable
@Parcelize
@@ -41,3 +26,29 @@ data class StubJson(
val versionCode: Int = -1,
val link: String = ""
) : Parcelable
@JsonClass(generateAdapter = true)
data class ModuleJson(
val id: String,
val last_update: Long,
val prop_url: String,
val zip_url: 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)
data class CommitInfo(
val sha: String
)
@JsonClass(generateAdapter = true)
data class BranchInfo(
val commit: CommitInfo
)

View File

@@ -1,41 +0,0 @@
package com.topjohnwu.magisk.core.model.module
abstract class BaseModule : Comparable<BaseModule> {
abstract var id: String
protected set
abstract var name: String
protected set
abstract var author: String
protected set
abstract var version: String
protected set
abstract var versionCode: Int
protected set
abstract var description: String
protected set
@Throws(NumberFormatException::class)
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: BaseModule) = name.compareTo(other.name, true)
}

View File

@@ -0,0 +1,89 @@
package com.topjohnwu.magisk.core.model.module
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class LocalModule(path: String) : Module() {
override var id: String = ""
override var name: String = ""
override var author: String = ""
override var version: String = ""
override var versionCode: Int = -1
override var description: String = ""
private val removeFile = SuFile(path, "remove")
private val disableFile = SuFile(path, "disable")
private val updateFile = SuFile(path, "update")
private val ruleFile = SuFile(path, "sepolicy.rule")
val updated: Boolean get() = updateFile.exists()
var enable: Boolean
get() = !disableFile.exists()
set(enable) {
val dir = "$PERSIST/$id"
if (enable) {
disableFile.delete()
if (Const.Version.atLeast_21_2())
Shell.su("copy_sepolicy_rules").submit()
else
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
} else {
!disableFile.createNewFile()
if (Const.Version.atLeast_21_2())
Shell.su("copy_sepolicy_rules").submit()
else
Shell.su("rm -rf $dir").submit()
}
}
var remove: Boolean
get() = removeFile.exists()
set(remove) {
if (remove) {
removeFile.createNewFile()
if (Const.Version.atLeast_21_2())
Shell.su("copy_sepolicy_rules").submit()
else
Shell.su("rm -rf $PERSIST/$id").submit()
} else {
!removeFile.delete()
if (Const.Version.atLeast_21_2())
Shell.su("copy_sepolicy_rules").submit()
else
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
}
}
init {
runCatching {
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
}
if (id.isEmpty()) {
val sep = path.lastIndexOf('/')
id = path.substring(sep + 1)
}
if (name.isEmpty()) {
name = id
}
}
companion object {
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
suspend fun installed() = withContext(Dispatchers.IO) {
SuFile(Const.MAGISK_PATH)
.listFiles { _, name -> name != "lost+found" && name != ".core" }
.orEmpty()
.filter { !it.isFile }
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
.sortedBy { it.name.toLowerCase() }
}
}
}

View File

@@ -1,77 +1,41 @@
package com.topjohnwu.magisk.core.model.module
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
abstract class Module : Comparable<Module> {
abstract var id: String
protected set
abstract var name: String
protected set
abstract var author: String
protected set
abstract var version: String
protected set
abstract var versionCode: Int
protected set
abstract var description: String
protected set
class Module(path: String) : BaseModule() {
override var id: String = ""
override var name: String = ""
override var author: String = ""
override var version: String = ""
override var versionCode: Int = -1
override var description: String = ""
@Throws(NumberFormatException::class)
protected fun parseProps(props: List<String>) {
for (line in props) {
val prop = line.split("=".toRegex(), 2).map { it.trim() }
if (prop.size != 2)
continue
private val removeFile = SuFile(path, "remove")
private val disableFile = SuFile(path, "disable")
private val updateFile = SuFile(path, "update")
private val ruleFile = SuFile(path, "sepolicy.rule")
val key = prop[0]
val value = prop[1]
if (key.isEmpty() || key[0] == '#')
continue
val updated: Boolean get() = updateFile.exists()
var enable: Boolean
get() = !disableFile.exists()
set(enable) {
val dir = "$PERSIST/$id"
if (enable) {
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
disableFile.delete()
} else {
Shell.su("rm -rf $dir").submit()
!disableFile.createNewFile()
when (key) {
"id" -> id = value
"name" -> name = value
"version" -> version = value
"versionCode" -> versionCode = value.toInt()
"author" -> author = value
"description" -> description = value
}
}
var remove: Boolean
get() = removeFile.exists()
set(remove) {
if (remove) {
Shell.su("rm -rf $PERSIST/$id").submit()
removeFile.createNewFile()
} else {
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
!removeFile.delete()
}
}
init {
runCatching {
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
}
if (id.isEmpty()) {
val sep = path.lastIndexOf('/')
id = path.substring(sep + 1)
}
if (name.isEmpty()) {
name = id
}
}
companion object {
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
suspend fun installed() = withContext(Dispatchers.IO) {
SuFile(Const.MAGISK_PATH)
.listFiles { _, name -> name != "lost+found" && name != ".core" }
.orEmpty()
.filter { !it.isFile }
.map { Module("${Const.MAGISK_PATH}/${it.name}") }
.sortedBy { it.name.toLowerCase() }
}
}
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
}

View File

@@ -0,0 +1,66 @@
package com.topjohnwu.magisk.core.model.module
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.model.ModuleJson
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.legalFilename
import kotlinx.parcelize.Parcelize
import java.text.DateFormat
import java.util.*
@Entity(tableName = "modules")
@Parcelize
data class OnlineModule(
@PrimaryKey override var id: String,
override var name: String = "",
override var author: String = "",
override var version: String = "",
override var versionCode: Int = -1,
override var description: String = "",
val last_update: Long,
val prop_url: String,
val zip_url: String,
val notes_url: String
) : Module(), Parcelable {
private val svc: NetworkService get() = get()
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()
suspend fun notes() = svc.fetchString(notes_url)
@Throws(IllegalRepoException::class)
suspend fun load() {
try {
val rawProps = svc.fetchString(prop_url)
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,64 +0,0 @@
package com.topjohnwu.magisk.core.model.module
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.legalFilename
import kotlinx.android.parcel.Parcelize
import java.text.DateFormat
import java.util.*
@Entity(tableName = "repos")
@Parcelize
data class Repo(
@PrimaryKey override var id: String,
override var name: String,
override var author: String,
override var version: String,
override var versionCode: Int,
override var description: String,
var last_update: Long
) : BaseModule(), Parcelable {
private val stringRepo: StringRepository get() = get()
val lastUpdate get() = Date(last_update)
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
suspend fun readme() = stringRepo.getReadme(this)
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
constructor(id: String) : this(id, "", "", "", -1, "", 0)
@Throws(IllegalRepoException::class)
private fun loadProps(props: String) {
props.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.runCatching {
parseProps(this)
}.onFailure {
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
}
if (versionCode < 0) {
throw IllegalRepoException("Repo [$id] does not contain versionCode")
}
}
@Throws(IllegalRepoException::class)
suspend fun update(lastUpdate: Date? = null) {
lastUpdate?.let { last_update = it.time }
loadProps(stringRepo.getMetadata(this))
}
class IllegalRepoException(message: String) : Exception(message)
companion object {
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
}
}

View File

@@ -1,11 +1,13 @@
@file:SuppressLint("InlinedApi")
package com.topjohnwu.magisk.core.model.su
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.core.model.su.SuPolicy.Companion.INTERACTIVE
import com.topjohnwu.magisk.ktx.getLabel
data class SuPolicy(
var uid: Int,
val packageName: String,
@@ -38,7 +40,7 @@ fun SuPolicy.toMap() = mapOf(
fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
val uid = get("uid")?.toIntOrNull() ?: -1
val packageName = get("package_name").orEmpty()
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
val info = pm.getApplicationInfo(packageName, PackageManager.MATCH_UNINSTALLED_PACKAGES)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
@@ -59,7 +61,7 @@ fun Map<String, String>.toPolicy(pm: PackageManager): SuPolicy {
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): SuPolicy {
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
?: throw PackageManager.NameNotFoundException()
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
val info = pm.getApplicationInfo(pkg, PackageManager.MATCH_UNINSTALLED_PACKAGES)
return SuPolicy(
uid = info.uid,
packageName = pkg,

View File

@@ -7,14 +7,12 @@ import android.os.Bundle
import android.os.Process
import android.widget.Toast
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.ProviderCallHandler
import com.topjohnwu.magisk.R
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.toLog
import com.topjohnwu.magisk.core.model.su.toPolicy
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.startActivity
@@ -26,18 +24,13 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
object SuCallbackHandler : ProviderCallHandler {
object SuCallbackHandler {
const val REQUEST = "request"
const val LOG = "log"
const val NOTIFY = "notify"
const val TEST = "test"
override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
invoke(context.wrap(), method, extras)
return Bundle.EMPTY
}
operator fun invoke(context: Context, action: String?, data: Bundle?) {
data ?: return

View File

@@ -36,7 +36,7 @@ class SuRequestHandler(
if (policy.packageName == BuildConfig.APPLICATION_ID)
return false
when (Config.suAutoReponse) {
when (Config.suAutoResponse) {
Config.Value.SU_AUTO_DENY -> {
respond(SuPolicy.DENY, 0)
return false

View File

@@ -2,14 +2,13 @@ package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.net.Uri
import androidx.core.os.postDelayed
import androidx.core.net.toFile
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.displayName
import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.unzip
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
@@ -25,61 +24,50 @@ open class FlashZip(
private val logs: MutableList<String>
): KoinComponent {
val context: Context by inject()
private val installFolder = File(context.cacheDir, "flash").apply {
if (!exists()) mkdirs()
}
private val tmpFile: File = File(installFolder, "install.zip")
@Throws(IOException::class)
private fun unzipAndCheck(): Boolean {
val parentFile = tmpFile.parentFile ?: return false
tmpFile.unzip(parentFile, "META-INF/com/google/android", true)
val updaterScript = File(parentFile, "updater-script")
return Shell
.su("grep -q '#MAGISK' $updaterScript")
.exec()
.isSuccess
}
private val context: Context by inject()
private val installDir = File(context.cacheDir, "flash")
private lateinit var zipFile: File
@Throws(IOException::class)
private fun flash(): Boolean {
console.add("- Copying zip to temp directory")
installDir.deleteRecursively()
installDir.mkdirs()
runCatching {
mUri.inputStream().writeTo(tmpFile)
}.getOrElse {
when (it) {
is FileNotFoundException -> console.add("! Invalid Uri")
is IOException -> console.add("! Cannot copy to cache")
zipFile = if (mUri.scheme == "file") {
mUri.toFile()
} else {
File(installDir, "install.zip").also {
console.add("- Copying zip to temp directory")
try {
mUri.inputStream().writeTo(it)
} catch (e: IOException) {
when (e) {
is FileNotFoundException -> console.add("! Invalid Uri")
else -> console.add("! Cannot copy to cache")
}
throw e
}
}
throw it
}
val isMagiskModule = runCatching {
unzipAndCheck()
val isValid = runCatching {
zipFile.unzip(installDir, "META-INF/com/google/android", true)
val script = File(installDir, "updater-script")
script.readText().contains("#MAGISK")
}.getOrElse {
console.add("! Unzip error")
throw it
}
if (!isMagiskModule) {
console.add("! This zip is not a Magisk Module!")
if (!isValid) {
console.add("! This zip is not a Magisk module!")
return false
}
console.add("- Installing ${mUri.displayName}")
val parentFile = tmpFile.parent ?: return false
return Shell
.su(
"cd $parentFile",
"BOOTMODE=true sh update-binary dummy 1 $tmpFile"
)
.to(console, logs)
.exec().isSuccess
return Shell.su("sh $installDir/update-binary dummy 1 \"$zipFile\"")
.to(console, logs).exec().isSuccess
}
open suspend fun exec() = withContext(Dispatchers.IO) {
@@ -94,25 +82,7 @@ open class FlashZip(
Timber.e(e)
false
} finally {
Shell.su("cd /", "rm -rf ${tmpFile.parent} ${Const.TMP_FOLDER_PATH}").submit()
Shell.su("cd /", "rm -rf $installDir ${Const.TMPDIR}").submit()
}
}
class Uninstall(
uri: Uri,
console: MutableList<String>,
log: MutableList<String>
) : FlashZip(uri, console, log) {
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
UiThreadHandler.handler.postDelayed(3000) {
Shell.su("pm uninstall " + context.packageName).exec()
}
}
return success
}
}
}

View File

@@ -0,0 +1,168 @@
package com.topjohnwu.magisk.core.tasks
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.Provider
import com.topjohnwu.magisk.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.APKInstall
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignApk
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.ref.WeakReference
import java.security.SecureRandom
object HideAPK {
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
private const val ALPHADOTS = "$ALPHA....."
private const val APP_NAME = "Magisk"
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
// Some arbitrary limit
const val MAX_LABEL_LENGTH = 32
private val svc: NetworkService by inject()
private val Context.APK_URI get() = Provider.APK_URI(packageName)
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
private fun genPackageName(): String {
val random = SecureRandom()
val len = 5 + random.nextInt(15)
val builder = StringBuilder(len)
var next: Char
var prev = 0.toChar()
for (i in 0 until len) {
next = if (prev == '.' || i == 0 || i == len - 1) {
ALPHA[random.nextInt(ALPHA.length)]
} else {
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
}
builder.append(next)
prev = next
}
if (!builder.contains('.')) {
// Pick a random index and set it as dot
val idx = random.nextInt(len - 2)
builder[idx + 1] = '.'
}
return builder.toString()
}
fun patch(
context: Context,
apk: File, out: File,
pkg: String, label: CharSequence
): Boolean {
try {
val jar = JarMap.open(apk, true)
val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
return false
// Write apk changes
jar.getOutputStream(je).write(xml.bytes)
val keys = Keygen(context)
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
} catch (e: Exception) {
Timber.e(e)
return false
}
return true
}
private class WaitPackageReceiver(
private val pkg: String,
activity: Activity
) : BroadcastReceiver() {
private val activity = WeakReference(activity)
private fun launchApp(): Unit = activity.get()?.run {
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return
Config.suManager = if (pkg == APPLICATION_ID) "" else pkg
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 {
val stub = File(activity.cacheDir, "stub.apk")
try {
svc.fetchFile(Info.remote.stub.link).byteStream().writeTo(stub)
} catch (e: IOException) {
Timber.e(e)
return false
}
// Generate a new random package name and signature
val repack = File(activity.cacheDir, "patched.apk")
val pkg = genPackageName()
Config.keyStoreRaw = ""
if (!patch(activity, stub, repack, pkg, label))
return false
// Install and auto launch app
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(pkg, activity))
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
APKInstall.installHideResult(activity, repack)
return true
}
suspend fun hide(activity: Activity, label: String) {
val result = withContext(Dispatchers.IO) {
patchAndHide(activity, label)
}
if (!result) {
Utils.toast(R.string.failure, Toast.LENGTH_LONG)
}
}
fun restore(activity: Activity) {
val apk = DynAPK.current(activity)
APKInstall.registerInstallReceiver(activity, WaitPackageReceiver(APPLICATION_ID, activity))
Shell.su("adb_pm_install $apk").submit {
if (!it.isSuccess)
APKInstall.installHideResult(activity, apk)
}
}
}

View File

@@ -1,23 +1,23 @@
package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.widget.Toast
import androidx.annotation.WorkerThread
import androidx.core.os.postDelayed
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
import com.topjohnwu.magisk.ktx.reboot
import com.topjohnwu.magisk.ktx.symlink
import com.topjohnwu.magisk.ktx.withStreams
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell
@@ -29,187 +29,217 @@ import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import net.jpountz.lz4.LZ4FrameInputStream
import org.kamranzafar.jtar.TarEntry
import org.kamranzafar.jtar.TarHeader
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import org.koin.core.KoinComponent
import org.koin.core.get
import org.koin.core.inject
import timber.log.Timber
import java.io.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.*
import java.util.zip.ZipFile
abstract class MagiskInstallImpl : KoinComponent {
abstract class MagiskInstallImpl protected constructor(
protected val console: MutableList<String> = NOPList.getInstance(),
private val logs: MutableList<String> = NOPList.getInstance()
) : KoinComponent {
protected lateinit var installDir: File
private lateinit var srcBoot: String
private lateinit var zipUri: Uri
protected var installDir = File("xxx")
private lateinit var srcBoot: File
protected val console: MutableList<String>
private val logs: MutableList<String>
private var tarOut: TarOutputStream? = null
private val service: GithubRawServices by inject()
protected val context: Context by inject()
protected constructor() {
console = NOPList.getInstance()
logs = NOPList.getInstance()
}
protected constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
console = out
logs = err
zipUri = zip
installDir = File(get<Context>(Protected).filesDir.parent, "install")
"rm -rf $installDir".sh()
installDir.mkdirs()
}
private val shell = Shell.getShell()
private val service: NetworkService by inject()
protected val context: Context by inject(Protected)
private val useRootDir = shell.isRoot && Info.noDataExec
private fun findImage(): Boolean {
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
if (srcBoot.isEmpty()) {
val bootPath = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
if (bootPath.isEmpty()) {
console.add("! Unable to detect target image")
return false
}
console.add("- Target image: $srcBoot")
srcBoot = SuFile(bootPath)
console.add("- Target image: $bootPath")
return true
}
private fun findSecondaryImage(): Boolean {
private fun findSecondary(): Boolean {
val slot = "echo \$SLOT".fsh()
val target = if (slot == "_a") "_b" else "_a"
console.add("- Target slot: $target")
srcBoot = arrayOf(
val bootPath = arrayOf(
"SLOT=$target",
"find_boot_image",
"SLOT=$slot",
"echo \"\$BOOTIMAGE\"").fsh()
if (srcBoot.isEmpty()) {
if (bootPath.isEmpty()) {
console.add("! Unable to detect target image")
return false
}
console.add("- Target image: $srcBoot")
srcBoot = SuFile(bootPath)
console.add("- Target image: $bootPath")
return true
}
@Suppress("DEPRECATION")
private fun extractZip(): Boolean {
val arch: String
arch = if (Build.VERSION.SDK_INT >= 21) {
val abis = listOf(*Build.SUPPORTED_ABIS)
if (abis.contains("x86")) "x86" else "arm"
} else {
if (Build.CPU_ABI == "x86") "x86" else "arm"
}
private fun extractFiles(): Boolean {
console.add("- Device platform: ${Const.CPU_ABI}")
console.add("- Installing: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
console.add("- Device platform: " + Build.CPU_ABI)
installDir = File(context.filesDir.parent, "install")
installDir.deleteRecursively()
installDir.mkdirs()
try {
ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
lateinit var ze: ZipEntry
while (zi.nextEntry?.let { ze = it } != null) {
if (ze.isDirectory)
continue
var name: String? = null
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
for (n in names) {
ze.name.run {
if (startsWith(n)) {
name = substring(lastIndexOf('/') + 1)
}
}
name ?: continue
break
}
if (name == null && ze.name.startsWith("chromeos/"))
name = ze.name
name?.also {
val dest = if (installDir is SuFile)
SuFile(installDir, it)
else
File(installDir, it)
dest.parentFile!!.mkdirs()
SuFileOutputStream(dest).use { s -> zi.copyTo(s) }
} ?: continue
// Extract binaries
if (isRunningAsStub) {
val zf = ZipFile(DynAPK.current(context))
zf.entries().asSequence().filter {
!it.isDirectory && it.name.startsWith("lib/${Const.CPU_ABI_32}/")
}.forEach {
val n = it.name.substring(it.name.lastIndexOf('/') + 1)
val name = n.substring(3, n.length - 3)
val dest = File(installDir, name)
zf.getInputStream(it).writeTo(dest)
}
} else {
val libs = Const.NATIVE_LIB_DIR.listFiles { _, name ->
name.startsWith("lib") && name.endsWith(".so")
} ?: emptyArray()
for (lib in libs) {
val name = lib.name.substring(3, lib.name.length - 3)
symlink(lib.path, "$installDir/$name")
}
}
} catch (e: IOException) {
console.add("! Cannot unzip zip")
// Extract scripts
for (script in listOf("util_functions.sh", "boot_patch.sh", "addon.d.sh")) {
val dest = File(installDir, script)
context.assets.open(script).writeTo(dest)
}
// Extract chromeos tools
File(installDir, "chromeos").mkdir()
for (file in listOf("futility", "kernel_data_key.vbprivk", "kernel.keyblock")) {
val name = "chromeos/$file"
val dest = File(installDir, name)
context.assets.open(name).writeTo(dest)
}
} catch (e: Exception) {
console.add("! Unable to extract files")
Timber.e(e)
return false
}
val init64 = SuFile.open(installDir, "magiskinit64")
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
init64.renameTo(SuFile.open(installDir, "magiskinit"))
} else {
init64.delete()
if (useRootDir) {
// Move everything to tmpfs to workaround Samsung bullshit
SuFile(Const.TMPDIR).also {
arrayOf(
"rm -rf $it",
"mkdir -p $it",
"cp_readlink $installDir $it",
"rm -rf $installDir"
).sh()
installDir = it
}
}
"cd $installDir; chmod 755 *".sh()
return true
}
private fun newEntry(name: String, size: Long): TarEntry {
// Optimization for SuFile I/O streams to skip an internal trial and error
private fun installDirFile(name: String): File {
return if (useRootDir)
SuFile(installDir, name)
else
File(installDir, name)
}
private fun InputStream.cleanPump(out: OutputStream) = withStreams(this, out) { src, _ ->
src.copyTo(out)
}
private fun newTarEntry(name: String, size: Long): TarEntry {
console.add("-- Writing: $name")
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
}
@Throws(IOException::class)
private fun handleTar(input: InputStream, output: OutputStream): OutputStream {
private fun processTar(input: InputStream, output: OutputStream): OutputStream {
console.add("- Processing tar file")
val tarOut = TarOutputStream(output)
TarInputStream(input).use { tarIn ->
lateinit var entry: TarEntry
fun decompressedStream(): InputStream {
val src = if (entry.name.endsWith(".lz4")) LZ4FrameInputStream(tarIn) else tarIn
return object : FilterInputStream(src) {
override fun available() = 0 /* Workaround bug in LZ4FrameInputStream */
override fun close() { /* Never close src stream */ }
}
}
while (tarIn.nextEntry?.let { entry = it } != null) {
if (entry.name.contains("boot.img") ||
if (entry.name.startsWith("boot.img") ||
(Config.recovery && entry.name.contains("recovery.img"))) {
val name = entry.name
val name = entry.name.replace(".lz4", "")
console.add("-- Extracting: $name")
val extract = File(installDir, name)
FileOutputStream(extract).use { tarIn.copyTo(it) }
if (name.contains(".lz4")) {
console.add("-- Decompressing: $name")
"./magiskboot decompress $extract".sh()
}
val extract = installDirFile(name)
decompressedStream().cleanPump(SuFileOutputStream.open(extract))
} else if (entry.name.contains("vbmeta.img")) {
val rawData = decompressedStream().readBytes()
// Valid vbmeta.img should be at least 256 bytes
if (rawData.size < 256)
continue
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
console.add("-- Patching: vbmeta.img")
ByteBuffer.wrap(rawData).putInt(120, 3)
tarOut.putNextEntry(newTarEntry("vbmeta.img", rawData.size.toLong()))
tarOut.write(rawData)
} else {
console.add("-- Copying: " + entry.name)
console.add("-- Copying: ${entry.name}")
tarOut.putNextEntry(entry)
tarIn.copyTo(tarOut)
tarIn.copyTo(tarOut, bufferSize = 1024 * 1024)
}
}
val boot = SuFile.open(installDir, "boot.img")
val recovery = SuFile.open(installDir, "recovery.img")
if (recovery.exists() && boot.exists()) {
// Install Magisk to recovery
srcBoot = recovery.path
// Repack boot image to prevent restore
arrayOf(
"./magiskboot unpack boot.img",
"./magiskboot repack boot.img",
"./magiskboot cleanup",
"mv new-boot.img boot.img").sh()
SuFileInputStream(boot).use {
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
it.copyTo(tarOut)
}
boot.delete()
} else {
if (!boot.exists()) {
console.add("! No boot image found")
throw IOException()
}
srcBoot = boot.path
}
val boot = installDirFile("boot.img")
val recovery = installDirFile("recovery.img")
if (Config.recovery && recovery.exists() && boot.exists()) {
// Install to recovery
srcBoot = recovery
// Repack boot image to prevent auto restore
arrayOf(
"cd $installDir",
"./magiskboot unpack boot.img",
"./magiskboot repack boot.img",
"cat new-boot.img > boot.img",
"./magiskboot cleanup",
"rm -f new-boot.img",
"cd /").sh()
SuFileInputStream.open(boot).use {
tarOut.putNextEntry(newTarEntry("boot.img", boot.length()))
it.copyTo(tarOut)
}
boot.delete()
} else {
if (!boot.exists()) {
console.add("! No boot image found")
throw IOException()
}
srcBoot = boot
}
return tarOut
}
private fun handleFile(uri: Uri): Boolean {
val outStream: OutputStream
val outFile: MediaStoreUtils.UriFile
var outFile: MediaStoreUtils.UriFile? = null
// Process input file
try {
@@ -221,37 +251,52 @@ abstract class MagiskInstallImpl : KoinComponent {
return false
}
src.reset()
val alpha = "abcdefghijklmnopqrstuvwxyz"
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
val random = SecureRandom()
val filename = StringBuilder("magisk_patched_").run {
for (i in 1..5) {
append(alphaNum[random.nextInt(alphaNum.length)])
}
toString()
}
outStream = if (magic.contentEquals("ustar".toByteArray())) {
outFile = MediaStoreUtils.getFile("magisk_patched.tar")
handleTar(src, outFile.uri.outputStream())
// tar file
outFile = MediaStoreUtils.getFile("$filename.tar", true)
processTar(src, outFile!!.uri.outputStream())
} else {
// Raw image
srcBoot = File(installDir, "boot.img").path
// raw image
srcBoot = installDirFile("boot.img")
console.add("- Copying image to cache")
FileOutputStream(srcBoot).use { src.copyTo(it) }
outFile = MediaStoreUtils.getFile("magisk_patched.img")
outFile.uri.outputStream()
src.cleanPump(SuFileOutputStream.open(srcBoot))
outFile = MediaStoreUtils.getFile("$filename.img", true)
outFile!!.uri.outputStream()
}
}
} catch (e: IOException) {
console.add("! Process error")
outFile?.delete()
Timber.e(e)
return false
}
// Patch file
if (!patchBoot())
if (!patchBoot()) {
outFile!!.delete()
return false
}
// Output file
try {
val patched = SuFile.open(installDir, "new-boot.img")
val newBoot = installDirFile("new-boot.img")
if (outStream is TarOutputStream) {
val name = if (srcBoot.contains("recovery")) "recovery.img" else "boot.img"
outStream.putNextEntry(newEntry(name, patched.length()))
val name = if (srcBoot.path.contains("recovery")) "recovery.img" else "boot.img"
outStream.putNextEntry(newTarEntry(name, newBoot.length()))
}
withStreams(SuFileInputStream(patched), outStream) { src, out -> src.copyTo(out) }
patched.delete()
SuFileInputStream.open(newBoot).cleanPump(outStream)
newBoot.delete()
console.add("")
console.add("****************************")
@@ -260,114 +305,112 @@ abstract class MagiskInstallImpl : KoinComponent {
console.add("****************************")
} catch (e: IOException) {
console.add("! Failed to output to $outFile")
outFile!!.delete()
Timber.e(e)
return false
}
// Fix up binaries
srcBoot.delete()
if (shell.isRoot) {
"fix_env $installDir".sh()
} else {
"cp_readlink $installDir".sh()
}
return true
}
private fun patchBoot(): Boolean {
var srcNand = ""
if ("[ -c $srcBoot ] && nanddump -f boot.img $srcBoot".sh().isSuccess) {
srcNand = srcBoot
srcBoot = File(installDir, "boot.img").path
}
var isSigned = false
try {
SuFileInputStream(srcBoot).use {
isSigned = SignBoot.verifySignature(it, null)
if (isSigned) {
console.add("- Boot image is signed with AVB 1.0")
if (srcBoot.let { it !is SuFile || !it.isCharacter }) {
try {
SuFileInputStream.open(srcBoot).use {
if (SignBoot.verifySignature(it, null)) {
isSigned = true
console.add("- Boot image is signed with AVB 1.0")
}
}
} catch (e: IOException) {
console.add("! Unable to check signature")
Timber.e(e)
return false
}
} catch (e: IOException) {
console.add("! Unable to check signature")
}
val newBoot = installDirFile("new-boot.img")
if (!useRootDir) {
// Create output files before hand
newBoot.createNewFile()
File(installDir, "stock_boot.img").createNewFile()
}
val cmds = arrayOf(
"cd $installDir",
"KEEPFORCEENCRYPT=${Config.keepEnc} " +
"KEEPVERITY=${Config.keepVerity} " +
"RECOVERYMODE=${Config.recovery} " +
"sh boot_patch.sh $srcBoot")
if (!cmds.sh().isSuccess)
return false
}
if (!("KEEPFORCEENCRYPT=${Config.keepEnc} KEEPVERITY=${Config.keepVerity} " +
"RECOVERYMODE=${Config.recovery} sh update-binary " +
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
return false
}
val job = shell.newJob().add("./magiskboot cleanup", "cd /")
if (srcNand.isNotEmpty()) {
srcBoot = srcNand
}
val job = Shell.sh(
"./magiskboot cleanup",
"mv bin/busybox busybox",
"rm -rf magisk.apk bin boot.img update-binary",
"cd /")
val patched = File(installDir, "new-boot.img")
if (isSigned) {
console.add("- Signing boot image with verity keys")
val signed = File(installDir, "signed.img")
val signed = File.createTempFile("signed", ".img", context.cacheDir)
try {
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
val src = SuFileInputStream.open(newBoot).buffered()
val out = signed.outputStream().buffered()
withStreams(src, out) { _, _ ->
SignBoot.doSignature(null, null, src, out, "/boot")
}
} catch (e: IOException) {
console.add("! Unable to sign image")
Timber.e(e)
return false
}
job.add("mv -f $signed $patched")
job.add("cat $signed > $newBoot", "rm -f $signed")
}
job.exec()
return true
}
private fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false
"run_migrations".sh()
return true
}
private fun flashBoot() = "direct_install $installDir $srcBoot".sh().isSuccess
private suspend fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl")
try {
withStreams(service.fetchBootctl().byteStream(), SuFileOutputStream(bootctl)) {
it, out -> it.copyTo(out)
}
val bootctl = File.createTempFile("bootctl", null, context.cacheDir)
service.fetchBootctl().byteStream().writeTo(bootctl)
"post_ota $bootctl".sh()
} catch (e: IOException) {
console.add("! Unable to download bootctl")
Timber.e(e)
return false
}
"post_ota ${bootctl.parent}".sh()
console.add("***************************************")
console.add(" Next reboot will boot to second slot!")
console.add("***************************************")
return true
}
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
private fun String.sh() = shell.newJob().add(this).to(console, logs).exec()
private fun Array<String>.sh() = shell.newJob().add(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(shell, this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(shell, *this)
protected fun doPatchFile(patchFile: Uri) = extractZip() && handleFile(patchFile)
protected fun doPatchFile(patchFile: Uri) = extractFiles() && handleFile(patchFile)
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
protected fun direct() = findImage() && extractFiles() && patchBoot() && flashBoot()
protected suspend fun secondSlot() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
findSecondary() && extractFiles() && patchBoot() && flashBoot() && postOTA()
protected fun fixEnv(zip: Uri): Boolean {
installDir = SuFile("/data/adb/magisk")
Shell.su("rm -rf /data/adb/magisk/*").exec()
zipUri = zip
return extractZip() && Shell.su("fix_env").exec().isSuccess
}
protected fun fixEnv() = extractFiles() && "fix_env $installDir".sh().isSuccess
protected fun uninstall() = "run_uninstaller ${AssetHack.apk}".sh().isSuccess
@WorkerThread
protected abstract suspend fun operations(): Boolean
@@ -375,11 +418,10 @@ abstract class MagiskInstallImpl : KoinComponent {
open suspend fun exec() = withContext(Dispatchers.IO) { operations() }
}
sealed class MagiskInstaller(
file: Uri,
abstract class MagiskInstaller(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(file, console, logs) {
) : MagiskInstallImpl(console, logs) {
override suspend fun exec(): Boolean {
val success = super.exec()
@@ -393,46 +435,64 @@ sealed class MagiskInstaller(
}
class Patch(
file: Uri,
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(file, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = doPatchFile(uri)
}
class SecondSlot(
file: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(file, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = secondSlot()
}
class Direct(
file: Uri,
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(file, console, logs) {
) : MagiskInstaller(console, logs) {
override suspend fun operations() = direct()
}
}
class Emulator(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstaller(console, logs) {
override suspend fun operations() = fixEnv()
}
class EnvFixTask(
private val zip: Uri
) : MagiskInstallImpl() {
override suspend fun operations() = fixEnv(zip)
class Uninstall(
console: MutableList<String>,
logs: MutableList<String>
) : MagiskInstallImpl(console, logs) {
override suspend fun operations() = uninstall()
override suspend fun exec(): Boolean {
val success = super.exec()
LocalBroadcastManager.getInstance(context).sendBroadcast(Intent(EnvFixDialog.DISMISS))
Utils.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
return success
override suspend fun exec(): Boolean {
val success = super.exec()
if (success) {
UiThreadHandler.handler.postDelayed(3000) {
Shell.su("pm uninstall ${context.packageName}").exec()
}
}
return success
}
}
class FixEnv(private val callback: () -> Unit) : MagiskInstallImpl() {
override suspend fun operations() = fixEnv()
override suspend fun exec(): Boolean {
val success = super.exec()
callback()
Utils.toast(
if (success) R.string.reboot_delay_toast else R.string.setup_fail,
Toast.LENGTH_LONG
)
if (success)
UiThreadHandler.handler.postDelayed(5000) { reboot() }
return success
}
}
}

View File

@@ -1,139 +0,0 @@
package com.topjohnwu.magisk.core.tasks
import android.content.Context
import android.os.Build.VERSION.SDK_INT
import android.widget.Toast
import com.topjohnwu.magisk.R
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.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignApk
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 java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.security.SecureRandom
object PatchAPK {
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
private const val ALPHADOTS = "$ALPHA....."
private const val APP_ID = "com.topjohnwu.magisk"
private const val APP_NAME = "Magisk Manager"
// Some arbitrary limit
const val MAX_LABEL_LENGTH = 32
private fun genPackageName(): CharSequence {
val random = SecureRandom()
val len = 5 + random.nextInt(15)
val builder = StringBuilder(len)
var next: Char
var prev = 0.toChar()
for (i in 0 until len) {
next = if (prev == '.' || i == len - 1) {
ALPHA[random.nextInt(ALPHA.length)]
} else {
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
}
builder.append(next)
prev = next
}
if (!builder.contains('.')) {
// Pick a random index and set it as dot
val idx = random.nextInt(len - 1)
builder[idx] = '.'
}
return builder
}
fun patch(
context: Context,
apk: String, out: String,
pkg: CharSequence, label: CharSequence
): Boolean {
try {
val jar = JarMap.open(apk)
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je))
if (!xml.findAndPatch(APP_ID to pkg.toString(), APP_NAME to label.toString()))
return false
// Write apk changes
jar.getOutputStream(je).write(xml.bytes)
val keys = Keygen(context)
SignApk.sign(keys.cert, keys.key, jar, FileOutputStream(out))
} catch (e: Exception) {
Timber.e(e)
return false
}
return true
}
private suspend fun patchAndHide(context: Context, label: String): Boolean {
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
val src = if (dlStub) {
val stub = File(context.cacheDir, "stub.apk")
val svc = get<GithubRawServices>()
try {
svc.fetchFile(Info.remote.stub.link).byteStream().use {
it.writeTo(stub)
}
} catch (e: IOException) {
Timber.e(e)
return false
}
stub.path
} else {
context.packageCodePath
}
// Generate a new random package name and signature
val repack = File(context.cacheDir, "patched.apk")
val pkg = genPackageName()
Config.keyStoreRaw = ""
if (!patch(context, src, repack.path, pkg, label))
return false
// Install the application
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
return false
Config.suManager = pkg.toString()
Config.export()
Shell.su("pm uninstall $APP_ID").submit()
return true
}
fun hideManager(context: Context, label: String) {
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
GlobalScope.launch {
val result = withContext(Dispatchers.IO) {
patchAndHide(context, label)
}
if (!result)
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
}
}
}

View File

@@ -1,119 +1,42 @@
package com.topjohnwu.magisk.core.tasks
import com.squareup.moshi.JsonClass
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.synchronized
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.HashSet
class RepoUpdater(
private val api: GithubApiServices,
private val svc: NetworkService,
private val repoDB: RepoDao
) {
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
private suspend fun forcedReload(cached: MutableSet<String>) = coroutineScope {
cached.forEach {
launch {
val repo = repoDB.getRepo(it)!!
try {
repo.update()
repoDB.addRepo(repo)
} catch (e: Repo.IllegalRepoException) {
Timber.e(e)
}
}
}
}
private suspend fun loadRepos(
repos: List<GithubRepoInfo>,
cached: MutableSet<String>
) = coroutineScope {
repos.forEach {
// Skip submission
if (it.id == "submission")
return@forEach
launch {
val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id)
try {
repo.update(it.pushDate)
repoDB.addRepo(repo)
} catch (e: Repo.IllegalRepoException) {
Timber.e(e)
}
}
}
}
private enum class PageResult {
SUCCESS,
CACHED,
ERROR
}
private suspend fun loadPage(
cached: MutableSet<String>,
page: Int = 1,
etag: String = ""
): PageResult = coroutineScope {
runCatching {
val result = api.fetchRepos(page, etag)
result.run {
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
return@coroutineScope PageResult.CACHED
if (!isSuccessful)
return@coroutineScope PageResult.ERROR
if (page == 1)
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
val repoLoad = async { loadRepos(body()!!, cached) }
val next = if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) {
async { loadPage(cached, page + 1) }
} else {
async { PageResult.SUCCESS }
}
repoLoad.await()
return@coroutineScope next.await()
}
}.getOrElse {
Timber.e(it)
PageResult.ERROR
}
}
suspend fun run(forced: Boolean) = withContext(Dispatchers.IO) {
val cached = HashSet(repoDB.repoIDList).synchronized()
when (loadPage(cached, etag = repoDB.etagKey)) {
PageResult.CACHED -> if (forced) forcedReload(cached)
PageResult.SUCCESS -> repoDB.removeRepos(cached)
PageResult.ERROR -> Unit
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)
}
}
}
private val dateFormat: SimpleDateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@JsonClass(generateAdapter = true)
data class GithubRepoInfo(
val name: String,
val pushed_at: String
) {
val id get() = name
@Transient
val pushDate = dateFormat.parse(pushed_at)!!
}

View File

@@ -7,10 +7,12 @@ import android.util.Base64OutputStream
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.signing.CryptoUtils.readCertificate
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
import com.topjohnwu.signing.KeyData
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.math.BigInteger
import java.security.KeyPairGenerator
@@ -58,10 +60,10 @@ class Keygen(context: Context) : CertKeyProvider {
class TestProvider : CertKeyProvider {
override val cert by lazy {
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
readCertificate(ByteArrayInputStream(KeyData.testCert()))
}
override val key by lazy {
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
}
}

View File

@@ -3,20 +3,16 @@
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import android.util.DisplayMetrics
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.AssetHack
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.ResMgr
import com.topjohnwu.magisk.core.addAssetPath
import com.topjohnwu.magisk.ktx.langTagToLocale
import com.topjohnwu.magisk.ktx.toLangTag
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.Comparator
import kotlin.collections.ArrayList
var currentLocale: Locale = Locale.getDefault()
@@ -30,11 +26,8 @@ suspend fun availableLocales() = cachedLocales ?:
withContext(Dispatchers.Default) {
val compareId = R.string.app_changelog
// Create a completely new resource to prevent cross talk over app's configs
val asset = AssetManager::class.java.newInstance().apply { addAssetPath(ResMgr.apk) }
val config = Configuration(ResMgr.resource.configuration)
val metrics = DisplayMetrics().apply { setTo(ResMgr.resource.displayMetrics) }
val res = Resources(asset, metrics, config)
// Create a completely new resource to prevent cross talk over active configs
val res = AssetHack.newResource()
val locales = ArrayList<String>().apply {
// Add default locale
@@ -45,19 +38,17 @@ withContext(Dispatchers.Default) {
add("pt-BR")
// Then add all supported locales
addAll(res.assets.locales)
addAll(Resources.getSystem().assets.locales)
}.map {
it.langTagToLocale()
}.distinctBy {
config.setLocale(it)
res.updateConfiguration(config, metrics)
res.updateLocale(it)
res.getString(compareId)
}.sortedWith(Comparator { a, b ->
}.sortedWith { a, b ->
a.getDisplayName(a).compareTo(b.getDisplayName(b), true)
})
}
config.setLocale(defaultLocale)
res.updateConfiguration(config, metrics)
res.updateLocale(defaultLocale)
val defName = res.getString(R.string.system_default)
val names = ArrayList<String>(locales.size + 1)
@@ -79,6 +70,11 @@ fun Resources.updateConfig(config: Configuration = configuration) {
updateConfiguration(config, displayMetrics)
}
fun Resources.updateLocale(locale: Locale) {
configuration.setLocale(locale)
updateConfiguration(configuration, displayMetrics)
}
fun refreshLocale() {
val localeConfig = Config.locale
currentLocale = when {
@@ -86,5 +82,5 @@ fun refreshLocale() {
else -> localeConfig.langTagToLocale()
}
Locale.setDefault(currentLocale)
ResMgr.resource.updateConfig()
AssetHack.resource.updateConfig()
}

View File

@@ -1,6 +1,5 @@
package com.topjohnwu.magisk.core.utils
import android.annotation.SuppressLint
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
@@ -40,15 +39,17 @@ object MediaStoreUtils {
private val relativePath get() = relativePath(Config.downloadDir)
@RequiresApi(api = 29)
@RequiresApi(api = 30)
@Throws(IOException::class)
private fun insertFile(displayName: String): MediaStoreFile {
val values = ContentValues()
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
// before Android 11, MediaStore can not rename new file when file exists,
// insert will return null. use newFile() instead.
// When a file with the same name exists and was not created by us:
// - Before Android 11, insert will return null
// - On Android 11+, the system will automatically create a new name
// Thus the reason to restrict this method call to API 30+
val fileUri = cr.insert(tableUri, values) ?: throw IOException("Can't insert $displayName.")
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
@@ -65,14 +66,8 @@ object MediaStoreUtils {
throw IOException("Can't insert $displayName.")
}
@RequiresApi(api = 29)
private fun queryFile(displayName: String): UriFile? {
if (Build.VERSION.SDK_INT < 29) {
// Before official general purpose MediaStore API exists, fallback to file based I/O
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
parent.mkdirs()
return LegacyUriFile(File(parent, displayName))
}
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
@@ -92,16 +87,22 @@ object MediaStoreUtils {
return null
}
@SuppressLint("NewApi")
@Throws(IOException::class)
fun getFile(displayName: String): UriFile {
return queryFile(displayName) ?:
/* this code path will never happen pre 29 */ insertFile(displayName)
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
if (Build.VERSION.SDK_INT < 30) {
// Fallback to file based I/O pre Android 11
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
parent.mkdirs()
return LegacyUriFile(File(parent, displayName))
}
return if (skipQuery) insertFile(displayName)
else queryFile(displayName) ?: insertFile(displayName)
}
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
fun Uri.outputStream() = cr.openOutputStream(this) ?: throw FileNotFoundException()
fun Uri.outputStream() = cr.openOutputStream(this, "rwt") ?: throw FileNotFoundException()
val Uri.displayName: String get() {
if (scheme == "file") {

View File

@@ -1,59 +0,0 @@
package com.topjohnwu.magisk.core.utils
import android.content.Context
import android.os.Build
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.wrap
import com.topjohnwu.magisk.ktx.rawResource
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
class RootInit : Shell.Initializer() {
override fun onInit(context: Context, shell: Shell): Boolean {
return init(context.wrap(), shell)
}
fun init(context: Context, shell: Shell): Boolean {
shell.newJob().apply {
add("export SDK_INT=${Build.VERSION.SDK_INT}")
if (Const.Version.atLeast_20_4()) {
add("export MAGISKTMP=\$(magisk --path)/.magisk")
} else {
add("export MAGISKTMP=/sbin/.magisk")
}
if (Const.Version.atLeast_21_0()) {
add("export ASH_STANDALONE=1")
add("[ -x /data/adb/magisk/busybox ] && exec /data/adb/magisk/busybox sh")
} else {
add("export PATH=\"\$MAGISKTMP/busybox:\$PATH\"")
}
add(context.rawResource(R.raw.manager))
if (shell.isRoot) {
add(context.rawResource(R.raw.util_functions))
}
add("mm_init")
}.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")
Info.isSAR = getBool("SYSTEM_ROOT")
Info.ramdisk = getBool("RAMDISKEXIST")
Info.isAB = getBool("ISAB")
Info.crypto = getVar("CRYPTOTYPE")
Info.isPixel = fastCmd("getprop ro.product.brand") == "google"
// Default presets
Config.recovery = getBool("RECOVERYMODE")
Config.keepVerity = getBool("KEEPVERITY")
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
return true
}
}

View File

@@ -0,0 +1,102 @@
package com.topjohnwu.magisk.core.utils
import android.content.Context
import android.os.Build
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.deviceProtectedContext
import com.topjohnwu.magisk.ktx.rawResource
import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.io.File
import java.util.jar.JarFile
abstract class BaseShellInit : Shell.Initializer() {
final override fun onInit(context: Context, shell: Shell): Boolean {
return init(context.wrap(), shell)
}
abstract fun init(context: Context, shell: Shell): Boolean
}
class BusyBoxInit : BaseShellInit() {
override fun init(context: Context, shell: Shell): Boolean {
shell.newJob().apply {
add("export ASH_STANDALONE=1")
val localBB: File
if (isRunningAsStub) {
if (!shell.isRoot)
return true
val jar = JarFile(DynAPK.current(context))
val bb = jar.getJarEntry("lib/${Const.CPU_ABI_32}/libbusybox.so")
localBB = context.deviceProtectedContext.cachedFile("busybox")
localBB.delete()
jar.getInputStream(bb).writeTo(localBB)
localBB.setExecutable(true)
} else {
localBB = File(Const.NATIVE_LIB_DIR, "libbusybox.so")
}
if (shell.isRoot) {
add("export MAGISKTMP=\$(magisk --path)/.magisk")
// Test if we can properly execute stuff in /data
Info.noDataExec = !shell.newJob().add("$localBB true").exec().isSuccess
}
if (Info.noDataExec) {
// Copy it out of /data to workaround Samsung bullshit
add(
"if [ -x \$MAGISKTMP/busybox/busybox ]; then",
" cp -af $localBB \$MAGISKTMP/busybox/busybox",
" exec \$MAGISKTMP/busybox/busybox sh",
"else",
" cp -af $localBB /dev/.busybox",
" exec /dev/.busybox sh",
"fi"
)
} else {
// Directly execute the file
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("export API=${Build.VERSION.SDK_INT}")
add(context.rawResource(R.raw.manager))
if (shell.isRoot) {
add(context.assets.open("util_functions.sh"))
}
add("app_init")
}.exec()
Const.MAGISKTMP = getVar("MAGISKTMP")
Info.isSAR = getBool("SYSTEM_ROOT")
Info.ramdisk = getBool("RAMDISKEXIST")
Info.isAB = getBool("ISAB")
Info.crypto = getVar("CRYPTOTYPE")
// Default presets
Config.recovery = getBool("RECOVERYMODE")
Config.keepVerity = getBool("KEEPVERITY")
Config.keepEnc = getBool("KEEPFORCEENCRYPT")
return true
}
}

View File

@@ -36,7 +36,7 @@ fun InputStream.unzip(folder: File, path: String, junkPath: Boolean) {
dest = SuFile(folder, name)
dest.parentFile!!.mkdirs()
}
SuFileOutputStream(dest).use { out -> zin.copyTo(out) }
SuFileOutputStream.open(dest).use { out -> zin.copyTo(out) }
}
} catch (e: IOException) {
e.printStackTrace()

View File

@@ -1,67 +0,0 @@
@file:JvmMultifileClass
package com.topjohnwu.magisk.data.database
import androidx.room.Dao
import androidx.room.Query
import com.topjohnwu.magisk.core.model.module.Repo
interface RepoBase {
fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo>
fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo>
@Query("SELECT * FROM repos WHERE id = :id AND versionCode > :versionCode LIMIT 1")
fun getUpdatableRepoById(id: String, versionCode: Int): Repo?
@Query("SELECT * FROM repos WHERE id = :id LIMIT 1")
fun getRepoById(id: String): Repo?
companion object {
const val LIMIT = 10
}
}
@Dao
interface RepoByUpdatedDao : RepoBase {
@Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
override fun getRepos(offset: Int, limit: Int): List<Repo>
@Query(
"""SELECT *
FROM repos
WHERE
(author LIKE '%' || :query || '%') ||
(name LIKE '%' || :query || '%') ||
(description LIKE '%' || :query || '%')
ORDER BY last_update DESC
LIMIT :limit
OFFSET :offset"""
)
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
}
@Dao
interface RepoByNameDao : RepoBase {
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
override fun getRepos(offset: Int, limit: Int): List<Repo>
@Query(
"""SELECT *
FROM repos
WHERE
(author LIKE '%' || :query || '%') ||
(name LIKE '%' || :query || '%') ||
(description LIKE '%' || :query || '%')
ORDER BY name COLLATE NOCASE
LIMIT :limit
OFFSET :offset"""
)
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
}

View File

@@ -2,72 +2,89 @@ package com.topjohnwu.magisk.data.database
import androidx.room.*
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.model.module.OnlineModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Database(version = 6, entities = [Repo::class, RepoEtag::class], exportSchema = false)
@Database(version = 8, entities = [OnlineModule::class], exportSchema = false)
abstract class RepoDatabase : RoomDatabase() {
abstract fun repoDao() : RepoDao
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
abstract fun repoByNameDao(): RepoByNameDao
}
@Dao
abstract class RepoDao(private val db: RepoDatabase) {
val repoIDList get() = getRepoID().map { it.id }
val repos: List<Repo> get() = when (Config.repoOrder) {
Config.Value.ORDER_NAME -> getReposNameOrder()
else -> getReposDateOrder()
}
var etagKey: String
set(value) = addEtagRaw(RepoEtag(0, value))
get() = etagRaw()?.key.orEmpty()
suspend fun clear() = withContext(Dispatchers.IO) { db.clearAllTables() }
@Query("SELECT * FROM repos ORDER BY last_update DESC")
protected abstract fun getReposDateOrder(): List<Repo>
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE")
protected abstract fun getReposNameOrder(): List<Repo>
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract fun addRepo(repo: Repo)
@Query("SELECT * FROM repos WHERE id = :id")
abstract fun getRepo(id: String): Repo?
@Query("SELECT id FROM repos")
protected abstract fun getRepoID(): List<RepoID>
abstract fun addModule(repo: OnlineModule)
@Delete
abstract fun removeRepo(repo: Repo)
abstract fun removeModule(repo: OnlineModule)
@Query("DELETE FROM repos WHERE id = :id")
abstract fun removeRepo(id: String)
@Query("DELETE FROM modules WHERE id = :id")
abstract fun removeModule(id: String)
@Query("DELETE FROM repos WHERE id IN (:idList)")
abstract fun removeRepos(idList: Collection<String>)
@Query("DELETE FROM modules WHERE id IN (:idList)")
abstract fun removeModules(idList: Collection<String>)
@Query("SELECT * FROM etag")
protected abstract fun etagRaw(): RepoEtag?
@Query("SELECT * FROM modules WHERE id = :id")
abstract fun getModule(id: String): OnlineModule?
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract fun addEtagRaw(etag: RepoEtag)
@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 RepoID(
@PrimaryKey val id: String
data class ModuleStub(
@PrimaryKey val id: String,
val last_update: Long
)
@Entity(tableName = "etag")
data class RepoEtag(
@PrimaryKey val id: Int,
val key: String
)

View File

@@ -1,87 +0,0 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.*
interface GithubPageServices {
@GET("stable.json")
suspend fun fetchStableUpdate(): UpdateInfo
@GET("beta.json")
suspend fun fetchBetaUpdate(): UpdateInfo
}
interface GithubRawServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
suspend fun fetchStableUpdate(): UpdateInfo
@GET("$MAGISK_FILES/master/beta.json")
suspend fun fetchBetaUpdate(): UpdateInfo
@GET("$MAGISK_FILES/canary/debug.json")
suspend fun fetchCanaryUpdate(): UpdateInfo
@GET
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
@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_MASTER/scripts/module_installer.sh")
@Streaming
suspend fun fetchInstaller(): ResponseBody
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
suspend fun fetchModuleFile(@Path(MODULE) id: String, @Path(FILE) file: String): String
//endregion
/**
* This method shall be used exclusively for fetching files from urls from previous requests.
* Him, who uses it in a wrong way, shall die in an eternal flame.
* */
@GET
@Streaming
suspend fun fetchFile(@Url url: String): ResponseBody
@GET
suspend fun fetchString(@Url url: String): String
companion object {
private const val REVISION = "revision"
private const val MODULE = "module"
private const val FILE = "file"
private const val MAGISK_FILES = "topjohnwu/magisk_files"
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
}
}
interface GithubApiServices {
@GET("repos")
suspend fun fetchRepos(
@Query("page") page: Int,
@Header(Const.Key.IF_NONE_MATCH) etag: String,
@Query("sort") sort: String = "pushed",
@Query("per_page") count: Int = 100
): Response<List<GithubRepoInfo>>
}

View File

@@ -0,0 +1,70 @@
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.RepoJson
import com.topjohnwu.magisk.core.model.UpdateInfo
import okhttp3.ResponseBody
import retrofit2.http.*
private const val REVISION = "revision"
private const val BRANCH = "branch"
private const val REPO = "repo"
const val MAGISK_FILES = "topjohnwu/magisk_files"
const val MAGISK_MAIN = "topjohnwu/Magisk"
interface GithubPageServices {
@GET("stable.json")
suspend fun fetchStableUpdate(): UpdateInfo
@GET("beta.json")
suspend fun fetchBetaUpdate(): 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_FILES@{$REVISION}/canary.json")
suspend fun fetchCanaryUpdate(@Path(REVISION) revision: String): UpdateInfo
@GET("$MAGISK_MAIN@{$REVISION}/scripts/module_installer.sh")
@Streaming
suspend fun fetchInstaller(@Path(REVISION) revision: String): ResponseBody
}
interface RawServices {
@GET
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
@GET
suspend fun fetchRepoInfo(@Url url: String): RepoJson
@GET
@Streaming
suspend fun fetchFile(@Url url: String): ResponseBody
@GET
suspend fun fetchString(@Url url: String): String
}
interface GithubApiServices {
@GET("repos/{$REPO}/branches/{$BRANCH}")
@Headers("Accept: application/vnd.github.v3+json")
suspend fun fetchBranch(
@Path(REPO, encoded = true) repo: String,
@Path(BRANCH) branch: String
): BranchInfo
}

View File

@@ -11,16 +11,14 @@ interface PreferenceModel {
val fileName: String
get() = "${context.packageName}_preferences"
val commitPrefs: Boolean
get() = false
val prefs: SharedPreferences
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
fun preferenceStrInt(
name: String,
default: Int,
writeDefault: Boolean = false,
commit: Boolean = commitPrefs
commit: Boolean = false
) = object: ReadWriteProperty<PreferenceModel, Int> {
val base = StringProperty(name, default.toString(), commit)
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
@@ -33,37 +31,37 @@ interface PreferenceModel {
fun preference(
name: String,
default: Boolean,
commit: Boolean = commitPrefs
commit: Boolean = false
) = BooleanProperty(name, default, commit)
fun preference(
name: String,
default: Float,
commit: Boolean = commitPrefs
commit: Boolean = false
) = FloatProperty(name, default, commit)
fun preference(
name: String,
default: Int,
commit: Boolean = commitPrefs
commit: Boolean = false
) = IntProperty(name, default, commit)
fun preference(
name: String,
default: Long,
commit: Boolean = commitPrefs
commit: Boolean = false
) = LongProperty(name, default, commit)
fun preference(
name: String,
default: String,
commit: Boolean = commitPrefs
commit: Boolean = false
) = StringProperty(name, default, commit)
fun preference(
name: String,
default: Set<String>,
commit: Boolean = commitPrefs
commit: Boolean = false
) = StringSetProperty(name, default, commit)
}

View File

@@ -1,39 +0,0 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.GithubPageServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
class MagiskRepository(
private val rawSvc: GithubRawServices,
private val pageSvc: GithubPageServices
) {
suspend fun fetchUpdate() = try {
var info = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> pageSvc.fetchStableUpdate()
Config.Value.BETA_CHANNEL -> pageSvc.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> rawSvc.fetchCanaryUpdate()
Config.Value.CUSTOM_CHANNEL -> rawSvc.fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}
if (info.magisk.versionCode < Info.env.magiskVersionCode &&
Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
info = pageSvc.fetchBetaUpdate()
}
Info.remote = info
info
} catch (e: IOException) {
Timber.e(e)
null
} catch (e: HttpException) {
Timber.e(e)
null
}
}

View File

@@ -0,0 +1,94 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Config.Value.BETA_CHANNEL
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.DEFAULT_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.model.MagiskJson
import com.topjohnwu.magisk.core.model.StubJson
import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.data.network.*
import retrofit2.HttpException
import timber.log.Timber
import java.io.IOException
class NetworkService(
private val pages: GithubPageServices,
private val raw: RawServices,
private val jsd: JSDelivrServices,
private val api: GithubApiServices
) {
suspend fun fetchUpdate() = safe {
var info = when (Config.updateChannel) {
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
BETA_CHANNEL -> fetchBetaUpdate()
CANARY_CHANNEL -> fetchCanaryUpdate()
CUSTOM_CHANNEL -> fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}
if (info.magisk.versionCode < Info.env.magiskVersionCode &&
Config.updateChannel == DEFAULT_CHANNEL
) {
Config.updateChannel = BETA_CHANNEL
info = fetchBetaUpdate()
}
info
}
// UpdateInfo
private suspend fun fetchStableUpdate() = pages.fetchStableUpdate()
private suspend fun fetchBetaUpdate() = pages.fetchBetaUpdate()
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
private suspend fun fetchCanaryUpdate(): UpdateInfo {
val sha = fetchCanaryVersion()
val info = jsd.fetchCanaryUpdate(sha)
fun genCDNUrl(name: String) = "${Const.Url.JS_DELIVR_URL}${MAGISK_FILES}@${sha}/${name}"
fun MagiskJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
fun StubJson.updateCopy() = copy(link = genCDNUrl(link))
return info.copy(
magisk = info.magisk.updateCopy(),
stub = info.stub.updateCopy()
)
}
private inline fun <T> safe(factory: () -> T): T? {
return try {
factory()
} catch (e: Exception) {
Timber.e(e)
null
}
}
private inline fun <T> wrap(factory: () -> T): T {
return try {
factory()
} catch (e: HttpException) {
throw IOException(e)
}
}
// Modules related
suspend fun fetchRepoInfo(url: String = Const.Url.OFFICIAL_REPO) = safe {
raw.fetchRepoInfo(url)
}
// 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 fetchString(url: String) = wrap { raw.fetchString(url) }
private suspend fun fetchCanaryVersion() = api.fetchBranch(MAGISK_FILES, "canary").commit.sha
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
}

View File

@@ -1,16 +0,0 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.data.network.GithubRawServices
class StringRepository(
private val api: GithubRawServices
) {
suspend fun getString(url: String) = api.fetchString(url)
suspend fun getMetadata(repo: Repo) = api.fetchModuleFile(repo.id, "module.prop")
suspend fun getReadme(repo: Repo) = api.fetchModuleFile(repo.id, "README.md")
}

View File

@@ -200,12 +200,12 @@ fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Draw
}
}
@BindingAdapter("app:icon")
@BindingAdapter("icon")
fun Button.setIconRes(res: Int) {
(this as MaterialButton).setIconResource(res)
}
@BindingAdapter("app:icon")
@BindingAdapter("icon")
fun Button.setIcon(drawable: Drawable) {
(this as MaterialButton).icon = drawable
}

View File

@@ -1,9 +1,9 @@
package com.topjohnwu.magisk.di
import android.content.Context
import android.os.Build
import androidx.preference.PreferenceManager
import com.topjohnwu.magisk.core.ResMgr
import com.topjohnwu.magisk.core.AssetHack
import com.topjohnwu.magisk.ktx.deviceProtectedContext
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -11,15 +11,9 @@ val SUTimeout = named("su_timeout")
val Protected = named("protected")
val applicationModule = module {
factory { ResMgr.resource }
factory { AssetHack.resource }
factory { get<Context>().packageManager }
factory(Protected) { createDEContext(get()) }
factory(Protected) { get<Context>().deviceProtectedContext }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
}
private fun createDEContext(context: Context): Context {
return if (Build.VERSION.SDK_INT >= 24)
context.createDeviceProtectedStorageContext()
else context
single { PreferenceManager.getDefaultSharedPreferences(get(Protected)) }
}

View File

@@ -17,8 +17,6 @@ val databaseModule = module {
single { StringDao() }
single { createRepoDatabase(get()) }
single { get<RepoDatabase>().repoDao() }
single { get<RepoDatabase>().repoByNameDao() }
single { get<RepoDatabase>().repoByUpdatedDao() }
single { createSuLogDatabase(get(Protected)).suLogDao() }
single { RepoUpdater(get(), get()) }
}

View File

@@ -3,12 +3,14 @@ package com.topjohnwu.magisk.di
import android.content.Context
import android.os.Build
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubPageServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.data.network.JSDelivrServices
import com.topjohnwu.magisk.data.network.RawServices
import com.topjohnwu.magisk.ktx.precomputedText
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
@@ -19,6 +21,7 @@ import okhttp3.Dns
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
@@ -29,16 +32,15 @@ import java.net.UnknownHostException
val networkingModule = module {
single { createOkHttpClient(get()) }
single { createRetrofit(get()) }
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
single { createApiService<RawServices>(get(), Const.Url.GITHUB_RAW_URL) }
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
single { createApiService<GithubPageServices>(get(), Const.Url.GITHUB_PAGE_URL) }
single { createApiService<JSDelivrServices>(get(), Const.Url.JS_DELIVR_URL) }
single { createMarkwon(get(), get()) }
}
private class DnsResolver(client: OkHttpClient) : Dns {
private var dohError = false
private val poisonedHosts = listOf("raw.githubusercontent.com")
private val doh by lazy {
DnsOverHttps.Builder().client(client)
.url(HttpUrl.get("https://cloudflare-dns.com/dns-query"))
@@ -58,16 +60,12 @@ private class DnsResolver(client: OkHttpClient) : Dns {
}
override fun lookup(hostname: String): List<InetAddress> {
return if (!dohError && Config.doh && poisonedHosts.contains(hostname)) {
if (Config.doh) {
try {
doh.lookup(hostname)
} catch (e: UnknownHostException) {
dohError = true
Dns.SYSTEM.lookup(hostname)
}
} else {
Dns.SYSTEM.lookup(hostname)
return doh.lookup(hostname)
} catch (e: UnknownHostException) {}
}
return Dns.SYSTEM.lookup(hostname)
}
}
@@ -75,10 +73,11 @@ private class DnsResolver(client: OkHttpClient) : Dns {
fun createOkHttpClient(context: Context): OkHttpClient {
val builder = OkHttpClient.Builder()
// val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
// level = HttpLoggingInterceptor.Level.HEADERS
// }
// builder.addInterceptor(httpLoggingInterceptor)
if (BuildConfig.DEBUG) {
builder.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BASIC
})
}
if (!Networking.init(context)) {
Info.hasGMS = false

View File

@@ -1,13 +1,11 @@
package com.topjohnwu.magisk.di
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.data.repository.NetworkService
import org.koin.dsl.module
val repositoryModule = module {
single { MagiskRepository(get(), get()) }
single { LogRepository(get()) }
single { StringRepository(get()) }
single { NetworkService(get(), get(), get(), get()) }
}

View File

@@ -20,7 +20,7 @@ val viewModelModules = module {
viewModel { HideViewModel() }
viewModel { HomeViewModel(get()) }
viewModel { LogViewModel(get()) }
viewModel { ModuleViewModel(get(), get(), get()) }
viewModel { ModuleViewModel(get(), get()) }
viewModel { SafetynetViewModel() }
viewModel { SettingsViewModel(get()) }
viewModel { SuperuserViewModel(get(), get()) }
@@ -29,6 +29,6 @@ val viewModelModules = module {
viewModel { MainViewModel() }
// Legacy
viewModel { (args: FlashFragmentArgs) -> FlashViewModel(args, get()) }
viewModel { (args: FlashFragmentArgs) -> FlashViewModel(args) }
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
}

View File

@@ -1,45 +0,0 @@
package com.topjohnwu.magisk.events
import android.Manifest
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.widget.Toast
import androidx.annotation.RequiresPermission
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.ActivityExecutor
import com.topjohnwu.magisk.arch.BaseUIActivity
import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.utils.Utils
class InstallExternalModuleEvent : ViewEvent(), ActivityExecutor {
@RequiresPermission(allOf = [Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE])
override fun invoke(activity: BaseUIActivity<*, *>) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
try {
activity.startActivityForResult(intent, Const.ID.FETCH_ZIP)
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
companion object {
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): NavDirections? {
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
val data = data.data
if (data != null) {
return MainDirections.actionFlashFragment(data, Const.Value.FLASH_ZIP)
}
}
return null
}
}
}

View File

@@ -1,8 +1,11 @@
package com.topjohnwu.magisk.events
import android.os.Build
import android.os.PowerManager
import android.view.ContextThemeWrapper
import android.view.MenuItem
import android.widget.PopupMenu
import androidx.core.content.getSystemService
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.superuser.Shell
@@ -13,6 +16,7 @@ object RebootEvent {
private fun reboot(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_reboot_normal -> systemReboot()
R.id.action_reboot_userspace -> systemReboot("userspace")
R.id.action_reboot_bootloader -> systemReboot("bootloader")
R.id.action_reboot_download -> systemReboot("download")
R.id.action_reboot_edl -> systemReboot("edl")
@@ -25,6 +29,9 @@ object RebootEvent {
fun inflateMenu(activity: BaseActivity): PopupMenu {
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true)
menu.menu.getItem(R.id.action_reboot_userspace).isVisible = true
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
menu.setOnMenuItemClickListener(::reboot)
return menu

View File

@@ -35,11 +35,9 @@ class SnackbarEvent private constructor(
) = Snackbar.make(view, message, length).apply(builder).show()
override fun invoke(activity: BaseUIActivity<*, *>) {
if (activity is BaseUIActivity<*, *>) {
snackbar(activity.snackbarView,
msg.getText(activity.resources).toString(),
length, builder)
}
snackbar(activity.snackbarView,
msg.getText(activity.resources).toString(),
length, builder)
}
}

View File

@@ -4,26 +4,32 @@ import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.view.View
import android.widget.Toast
import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.*
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.Repo
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.view.MarkDownWindow
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
import kotlinx.coroutines.launch
class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) = action(activity)
}
class OpenChangelogEvent(val item: Repo) : ViewEventWithScope(), ContextExecutor {
override fun invoke(context: Context) {
scope.launch {
MarkDownWindow.show(context, null, item::readme)
}
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)
}
}
@@ -55,9 +61,11 @@ class DieEvent : ViewEvent(), ActivityExecutor {
}
}
class ShowUIEvent : ViewEvent(), ActivityExecutor {
class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
: ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) {
activity.setContentView()
activity.setAccessibilityDelegate(delegate)
}
}
@@ -67,27 +75,16 @@ class RecreateEvent : ViewEvent(), ActivityExecutor {
}
}
class RequestFileEvent : ViewEvent(), ActivityExecutor {
class MagiskInstallFileEvent(private val callback: ActivityResultCallback)
: ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) {
Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE)
.also {
try {
activity.startActivityForResult(it, REQUEST_CODE)
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
}
companion object {
private const val REQUEST_CODE = 10
fun resolve(requestCode: Int, resultCode: Int, data: Intent?) = data
?.takeIf { resultCode == Activity.RESULT_OK }
?.takeIf { requestCode == REQUEST_CODE }
?.data
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("*/*")
try {
activity.startActivityForResult(intent, callback)
Utils.toast(R.string.patch_file_msg, Toast.LENGTH_LONG)
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
}
@@ -106,3 +103,22 @@ class AddHomeIconEvent : ViewEvent(), ContextExecutor {
Shortcuts.addHomeIcon(context)
}
}
class SelectModuleEvent : ViewEvent(), FragmentExecutor {
override fun invoke(fragment: BaseUIFragment<*, *>) {
val intent = Intent(Intent.ACTION_GET_CONTENT).setType("application/zip")
try {
fragment.apply {
activity.startActivityForResult(intent) { code, intent ->
if (code == Activity.RESULT_OK && intent != null) {
intent.data?.also {
MainDirections.actionFlashFragment(Const.Value.FLASH_ZIP, it).navigate()
}
}
}
}
} catch (e: ActivityNotFoundException) {
Utils.toast(R.string.app_not_found, Toast.LENGTH_SHORT)
}
}
}

View File

@@ -10,7 +10,9 @@ abstract class DialogEvent : ViewEvent(), ActivityExecutor {
protected lateinit var dialog: MagiskDialog
override fun invoke(activity: BaseUIActivity<*, *>) {
dialog = MagiskDialog(activity).apply(this::build).reveal()
dialog = MagiskDialog(activity)
.apply { setOwnerActivity(activity) }
.apply(this::build).reveal()
}
abstract fun build(dialog: MagiskDialog)

View File

@@ -1,15 +1,11 @@
package com.topjohnwu.magisk.events.dialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.download.Action.EnvFix
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject.Magisk
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.view.MagiskDialog
import kotlinx.coroutines.launch
class EnvFixDialog : DialogEvent() {
@@ -24,20 +20,17 @@ class EnvFixDialog : DialogEvent() {
.applyMessage(R.string.setup_msg)
.resetButtons()
.cancellable(false)
val lbm = LocalBroadcastManager.getInstance(dialog.context)
lbm.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
(dialog.ownerActivity as BaseActivity).lifecycleScope.launch {
MagiskInstaller.FixEnv {
dialog.dismiss()
lbm.unregisterReceiver(this)
}
}, IntentFilter(DISMISS))
DownloadService.start(dialog.context, Magisk(EnvFix))
}.exec()
}
}
}
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = android.R.string.cancel
}
.let { Unit }
.let { }
companion object {
const val DISMISS = "com.topjohnwu.magisk.ENV_DONE"

View File

@@ -1,41 +1,41 @@
package com.topjohnwu.magisk.events.dialog
import android.content.Context
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.ktx.res
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.MarkDownWindow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.koin.core.get
import org.koin.core.inject
import java.io.File
class ManagerInstallDialog : DialogEvent() {
class ManagerInstallDialog : MarkDownDialog() {
private val svc: NetworkService by inject()
override suspend fun getMarkdownText(): String {
val text = svc.fetchString(Info.remote.magisk.note)
// Cache the changelog
val context = get<Context>()
context.cacheDir.listFiles { _, name -> name.endsWith(".md") }.orEmpty().forEach {
it.delete()
}
File(context.cacheDir, "${Info.remote.magisk.versionCode}.md").writeText(text)
return text
}
override fun build(dialog: MagiskDialog) {
super.build(dialog)
with(dialog) {
val subject = Subject.Manager(Action.APK.Upgrade)
applyTitle(R.string.repo_install_title.res(R.string.app_name.res()))
applyMessage(R.string.repo_install_msg.res(subject.title))
setCancelable(true)
applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.install
onClick { DownloadService.start(context, subject) }
onClick { DownloadService.start(context, Subject.Manager()) }
}
if (Info.remote.app.note.isEmpty()) return
applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.app_changelog
onClick {
GlobalScope.launch(Dispatchers.Main.immediate) {
MarkDownWindow.show(context, null, Info.remote.app.note)
}
}
titleRes = android.R.string.cancel
}
}
}

View File

@@ -0,0 +1,45 @@
package com.topjohnwu.magisk.events.dialog
import android.view.LayoutInflater
import android.widget.TextView
import androidx.annotation.CallSuper
import androidx.lifecycle.lifecycleScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.view.MagiskDialog
import io.noties.markwon.Markwon
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
import org.koin.core.inject
import timber.log.Timber
import kotlin.coroutines.cancellation.CancellationException
abstract class MarkDownDialog : DialogEvent(), KoinComponent {
private val markwon: Markwon by inject()
abstract suspend fun getMarkdownText(): String
@CallSuper
override fun build(dialog: MagiskDialog) {
with(dialog) {
val view = LayoutInflater.from(context).inflate(R.layout.markdown_window_md2, null)
applyView(view)
(ownerActivity as BaseActivity).lifecycleScope.launch {
val tv = view.findViewById<TextView>(R.id.md_txt)
withContext(Dispatchers.IO) {
try {
markwon.setMarkdown(tv, getMarkdownText())
} catch (e: Exception) {
if (e is CancellationException)
throw e
Timber.e(e)
tv.post { tv.setText(R.string.download_file_error) }
}
}
}
}
}
}

View File

@@ -5,16 +5,16 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.Repo
import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.view.MagiskDialog
class ModuleInstallDialog(private val item: Repo) : DialogEvent() {
class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
override fun build(dialog: MagiskDialog) {
with(dialog) {
fun download(install: Boolean) {
val config = if (install) Action.Flash.Primary else Action.Download
val config = if (install) Action.Flash else Action.Download
val subject = Subject.Module(item, config)
DownloadService.start(context, subject)
}

View File

@@ -1,11 +1,9 @@
package com.topjohnwu.magisk.events.dialog
import android.app.ProgressDialog
import android.widget.Toast
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.ui.flash.FlashFragment
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.superuser.Shell
@@ -17,24 +15,20 @@ class UninstallDialog : DialogEvent() {
.applyMessage(R.string.uninstall_magisk_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.restore_img
preventDismiss = true
onClick { restore(dialog) }
onClick { restore() }
}
if (Info.remote.uninstaller.link.isNotEmpty()) {
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
titleRes = R.string.complete_uninstall
onClick { completeUninstall() }
}
}
}
private fun restore(dialog: MagiskDialog) {
dialog.applyTitle(R.string.restore_img)
.applyMessage(R.string.restore_img_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
title = ""
}
.cancellable(false)
@Suppress("DEPRECATION")
private fun restore() {
val dialog = ProgressDialog(dialog.context).apply {
setMessage(dialog.context.getString(R.string.restore_img_msg))
show()
}
Shell.su("restore_imgs").submit { result ->
dialog.dismiss()
@@ -47,7 +41,7 @@ class UninstallDialog : DialogEvent() {
}
private fun completeUninstall() {
DownloadService.start(dialog.context, Subject.Magisk(Action.Uninstall))
FlashFragment.uninstall()
}
}

View File

@@ -0,0 +1,193 @@
@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
}
}
}
}

View File

@@ -1,15 +1,17 @@
package com.topjohnwu.magisk.ktx
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.*
import android.content.pm.ServiceInfo
import android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS
import android.content.pm.ServiceInfo.FLAG_USE_APP_ZYGOTE
import android.content.res.Configuration
import android.content.res.Resources
import android.database.Cursor
@@ -19,8 +21,8 @@ import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.system.Os
import android.text.PrecomputedText
import android.view.View
import android.view.ViewGroup
@@ -39,11 +41,13 @@ import androidx.core.widget.TextViewCompat
import androidx.databinding.BindingAdapter
import androidx.fragment.app.Fragment
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.lifecycle.lifecycleScope
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.AssetHack
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.ResMgr
import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils
@@ -53,35 +57,31 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.lang.reflect.Method
import java.lang.reflect.Array as JArray
val packageName: String get() = get<Context>().packageName
val PackageInfo.processes
get() = activities?.processNames.orEmpty() +
services?.processNames.orEmpty() +
receivers?.processNames.orEmpty() +
providers?.processNames.orEmpty()
private lateinit var osSymlink: Method
private lateinit var os: Any
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
val ApplicationInfo.packageInfo: PackageInfo get() {
val pm = get<PackageManager>()
return try {
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
pm.getPackageInfo(packageName, request)
} catch (e: Exception) {
// Exceed binder data transfer limit, fetch each component type separately
pm.getPackageInfo(packageName, 0).apply {
runCatching { activities = pm.getPackageInfo(packageName, GET_ACTIVITIES).activities }
runCatching { services = pm.getPackageInfo(packageName, GET_SERVICES).services }
runCatching { receivers = pm.getPackageInfo(packageName, GET_RECEIVERS).receivers }
runCatching { providers = pm.getPackageInfo(packageName, GET_PROVIDERS).providers }
fun symlink(oldPath: String, newPath: String) {
if (SDK_INT >= 21) {
Os.symlink(oldPath, newPath)
} else {
if (!::osSymlink.isInitialized) {
os = Class.forName("libcore.io.Libcore").getField("os").get(null)!!
osSymlink = os.javaClass.getMethod("symlink", String::class.java, String::class.java)
}
osSymlink.invoke(os, oldPath, newPath)
}
}
val ServiceInfo.isIsolated get() = (flags and FLAG_ISOLATED_PROCESS) != 0
@get:SuppressLint("InlinedApi")
val ServiceInfo.useAppZygote get() = (flags and FLAG_USE_APP_ZYGOTE) != 0
fun Context.rawResource(id: Int) = resources.openRawResource(id)
fun Context.getBitmap(id: Int): Bitmap {
@@ -101,6 +101,11 @@ fun Context.getBitmap(id: Int): Bitmap {
return bitmap
}
val Context.deviceProtectedContext: Context get() =
if (SDK_INT >= 24) {
createDeviceProtectedStorageContext()
} else { this }
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun Intent.startActivityWithRoot() {
@@ -245,15 +250,13 @@ fun Context.colorStateListCompat(@ColorRes id: Int) = try {
null
}
fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id)
fun Context.drawableCompat(@DrawableRes id: Int) = AppCompatResources.getDrawable(this, id)
/**
* Pass [start] and [end] dimensions, function will return left and right
* with respect to RTL layout direction
*/
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
if (SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
) {
if (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
return end to start
}
return start to end
@@ -313,8 +316,21 @@ fun ViewGroup.startAnimations() {
)
}
val View.activity: Activity get() {
var context = context
while(true) {
if (context !is ContextWrapper)
error("View is not attached to activity")
if (context is Activity)
return context
context = context.baseContext
}
}
var View.coroutineScope: CoroutineScope
get() = getTag(R.id.coroutineScope) as? CoroutineScope ?: GlobalScope
get() = getTag(R.id.coroutineScope) as? CoroutineScope
?: (activity as? BaseActivity)?.lifecycleScope
?: GlobalScope
set(value) = setTag(R.id.coroutineScope, value)
@set:BindingAdapter("precomputedText")
@@ -364,6 +380,16 @@ var TextView.precomputedText: CharSequence
}
fun Int.dpInPx(): Int {
val scale = ResMgr.resource.displayMetrics.density
val scale = AssetHack.resource.displayMetrics.density
return (this * scale + 0.5).toInt()
}
@SuppressLint("PrivateApi")
fun getProperty(key: String, def: String): String {
runCatching {
val clazz = Class.forName("android.os.SystemProperties")
val get = clazz.getMethod("get", String::class.java, String::class.java)
return get.invoke(clazz, key, def) as String
}
return def
}

View File

@@ -6,14 +6,12 @@ import timber.log.Timber
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
inline fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
var entry: ZipEntry? = nextEntry
while (entry != null) {
callback(entry)
@@ -53,7 +51,7 @@ fun String.langTagToLocale(): Locale {
if (Build.VERSION.SDK_INT >= 21) {
return Locale.forLanguageTag(this)
} else {
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
val tok = split("[-_]".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (tok.isEmpty()) {
return Locale("")
}
@@ -110,32 +108,3 @@ fun Locale.toLangTag(): String {
fun SimpleDateFormat.parseOrNull(date: String) =
runCatching { parse(date) }.onFailure { Timber.e(it) }.getOrNull()
// Reflection hacks
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
private val getDeclaredMethod = Class::class.java.getMethod("getDeclaredMethod",
String::class.java, arrayOf<Class<*>>()::class.java)
private val getDeclaredField = Class::class.java.getMethod("getDeclaredField", String::class.java)
fun ClassLoader.forceLoadClass(name: String) =
runCatching { loadClass.invoke(this, name) }.getOrNull() as Class<*>?
fun Class<*>.forceGetDeclaredMethod(name: String, vararg types: Class<*>) =
(runCatching { getDeclaredMethod.invoke(this, name, types) }.getOrNull() as Method?)?.also {
it.isAccessible = true
}
fun Class<*>.forceGetDeclaredField(name: String) =
(runCatching { getDeclaredField.invoke(this, name) }.getOrNull() as Field?)?.also {
it.isAccessible = true
}
inline fun <reified T> T.forceGetClass(name: String) =
T::class.java.classLoader?.forceLoadClass(name)
fun Class<*>.forceGetField(name: String): Field? =
forceGetDeclaredField(name) ?: superclass?.forceGetField(name)
fun Class<*>.forceGetMethod(name: String, vararg types: Class<*>): Method? =
forceGetDeclaredMethod(name, *types) ?: superclass?.forceGetMethod(name, *types)

View File

@@ -1,6 +1,7 @@
package com.topjohnwu.magisk.ui
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.os.Build
import android.os.Bundle
import android.view.MenuItem
@@ -10,10 +11,7 @@ import android.view.WindowManager
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams
import androidx.navigation.NavDirections
import com.google.android.material.card.MaterialCardView
import com.topjohnwu.magisk.MainDirections
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIActivity
@@ -23,13 +21,12 @@ import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.databinding.ActivityMainMd2Binding
import com.topjohnwu.magisk.ktx.startAnimations
import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideableBehavior
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
class MainViewModel : BaseViewModel()
@@ -39,14 +36,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
override val viewModel by viewModel<MainViewModel>()
override val navHost: Int = R.id.main_nav_host
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
//implementation doesn't favor us against the design library, so on re-create it's often given
//upper hand.
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
binding.mainNavigation.setPadding(0)
}
private var isRootFragment = true
override fun onCreate(savedInstanceState: Bundle?) {
@@ -86,12 +75,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
setSupportActionBar(binding.mainToolbar)
binding.mainToolbarWrapper.updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = HideTopViewOnScrollBehavior<MaterialCardView>()
}
binding.mainBottomBar.updateLayoutParams<CoordinatorLayout.LayoutParams> {
behavior = HideBottomViewOnScrollBehavior<MaterialCardView>()
}
binding.mainNavigation.setOnNavigationItemSelectedListener {
getScreen(it.itemId)?.navigate()
true
@@ -100,9 +83,7 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
(currentFragment as? ReselectionTarget)?.onReselected()
}
binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
val section = if (intent.action == ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
val section = if (intent.action == Intent.ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
else intent.getStringExtra(Const.Key.OPEN_SECTION)
getScreen(section)?.navigate()
@@ -116,16 +97,11 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
override fun onResume() {
super.onResume()
binding.mainNavigation.menu.apply {
findItem(R.id.superuserFragment)?.isEnabled = Info.env.isActive
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
findItem(R.id.logFragment)?.isEnabled = Info.env.isActive
}
}
override fun onDestroy() {
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> onBackPressed()
@@ -214,7 +190,37 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
.applyTitle(R.string.unsupport_magisk_title)
.applyMessage(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
.cancellable(true)
.cancellable(false)
.reveal()
}
if (!Info.isEmulator && Info.env.isActive && System.getenv("PATH")
?.split(':')
?.filterNot { File("$it/magisk").exists() }
?.any { File("$it/su").exists() } == true) {
MagiskDialog(this)
.applyTitle(R.string.unsupport_general_title)
.applyMessage(R.string.unsupport_other_su_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
.cancellable(false)
.reveal()
}
if (applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0) {
MagiskDialog(this)
.applyTitle(R.string.unsupport_general_title)
.applyMessage(R.string.unsupport_system_app_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
.cancellable(false)
.reveal()
}
if (applicationInfo.flags and ApplicationInfo.FLAG_EXTERNAL_STORAGE != 0) {
MagiskDialog(this)
.applyTitle(R.string.unsupport_general_title)
.applyMessage(R.string.unsupport_external_storage_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { titleRes = android.R.string.ok }
.cancellable(false)
.reveal()
}
}
@@ -238,11 +244,4 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
.reveal()
}
}
companion object {
private val ACTION_APPLICATION_PREFERENCES get() =
if (Build.VERSION.SDK_INT >= 24) Intent.ACTION_APPLICATION_PREFERENCES
else "???"
}
}

View File

@@ -17,13 +17,12 @@ import com.topjohnwu.magisk.ui.MainActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import com.topjohnwu.magisk.MainDirections.Companion.actionFlashFragment as toFlash
import com.topjohnwu.magisk.ui.flash.FlashFragmentArgs as args
class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>() {
override val layoutRes = R.layout.fragment_flash_md2
override val viewModel by viewModel<FlashViewModel> {
parametersOf(args.fromBundle(requireArguments()))
parametersOf(FlashFragmentArgs.fromBundle(requireArguments()))
}
private var defaultOrientation = -1
@@ -32,6 +31,10 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
super.onStart()
setHasOptionsMenu(true)
activity.setTitle(R.string.flash_screen_title)
viewModel.subtitle.observe(this) {
activity.supportActionBar?.setSubtitle(it)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@@ -74,7 +77,7 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
companion object {
private fun createIntent(context: Context, args: args) =
private fun createIntent(context: Context, args: FlashFragmentArgs) =
NavDeepLinkBuilder(context)
.setGraph(R.navigation.main)
.setComponentName(MainActivity::class.java.cmp(context.packageName))
@@ -87,59 +90,34 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
/* Flashing is understood as installing / flashing magisk itself */
fun flashIntent(context: Context, file: Uri, isSecondSlot: Boolean, id: Int = -1) = args(
installer = file,
action = flashType(isSecondSlot),
dismissId = id
).let { createIntent(context, it) }
fun flash(file: Uri, isSecondSlot: Boolean, id: Int) = toFlash(
installer = file,
action = flashType(isSecondSlot),
dismissId = id
fun flash(isSecondSlot: Boolean) = toFlash(
action = flashType(isSecondSlot)
).let { BaseUIActivity.postDirections(it) }
/* Patching is understood as injecting img files with magisk */
fun patchIntent(context: Context, file: Uri, uri: Uri, id: Int = -1) = args(
installer = file,
fun patch(uri: Uri) = toFlash(
action = Const.Value.PATCH_FILE,
additionalData = uri,
dismissId = id
).let { createIntent(context, it) }
fun patch(file: Uri, uri: Uri, id: Int) = toFlash(
installer = file,
action = Const.Value.PATCH_FILE,
additionalData = uri,
dismissId = id
additionalData = uri
).let { BaseUIActivity.postDirections(it) }
/* Uninstalling is understood as removing magisk entirely */
fun uninstallIntent(context: Context, file: Uri, id: Int = -1) = args(
installer = file,
action = Const.Value.UNINSTALL,
dismissId = id
).let { createIntent(context, it) }
fun uninstall(file: Uri, id: Int) = toFlash(
installer = file,
action = Const.Value.UNINSTALL,
dismissId = id
fun uninstall() = toFlash(
action = Const.Value.UNINSTALL
).let { BaseUIActivity.postDirections(it) }
/* Installing is understood as flashing modules / zips */
fun installIntent(context: Context, file: Uri, id: Int = -1) = args(
installer = file,
fun installIntent(context: Context, file: Uri, id: Int = -1) = FlashFragmentArgs(
action = Const.Value.FLASH_ZIP,
additionalData = file,
dismissId = id
).let { createIntent(context, it) }
fun install(file: Uri, id: Int) = toFlash(
installer = file,
action = Const.Value.FLASH_ZIP,
additionalData = file,
dismissId = id
).let { BaseUIActivity.postDirections(it) }
}

View File

@@ -1,9 +1,10 @@
package com.topjohnwu.magisk.ui.flash
import android.content.res.Resources
import android.net.Uri
import android.view.MenuItem
import androidx.databinding.Bindable
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
@@ -11,33 +12,29 @@ import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.RvBindingAdapter
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FlashViewModel(
args: FlashFragmentArgs,
private val resources: Resources
) : BaseViewModel() {
class FlashViewModel(args: FlashFragmentArgs) : BaseViewModel() {
@get:Bindable
var showReboot = Shell.rootAccess()
set(value) = set(value, field, { field = it }, BR.showReboot)
@get:Bindable
var behaviorText = resources.getString(R.string.flashing)
set(value) = set(value, field, { field = it }, BR.behaviorText)
private val _subtitle = MutableLiveData(R.string.flashing)
val subtitle get() = _subtitle as LiveData<Int>
val adapter = RvBindingAdapter<ConsoleItem>()
val items = diffListOf<ConsoleItem>()
@@ -53,33 +50,35 @@ class FlashViewModel(
}
init {
args.dismissId.takeIf { it != -1 }?.also {
Notifications.mgr.cancel(it)
}
val (installer, action, uri) = args
startFlashing(installer, uri, action)
val (action, uri, id) = args
if (id != -1)
Notifications.mgr.cancel(id)
startFlashing(action, uri)
}
private fun startFlashing(installer: Uri, uri: Uri?, action: String) {
private fun startFlashing(action: String, uri: Uri?) {
viewModelScope.launch {
val result = when (action) {
Const.Value.FLASH_ZIP -> {
FlashZip(installer, outItems, logItems).exec()
FlashZip(uri!!, outItems, logItems).exec()
}
Const.Value.UNINSTALL -> {
showReboot = false
FlashZip.Uninstall(installer, outItems, logItems).exec()
MagiskInstaller.Uninstall(outItems, logItems).exec()
}
Const.Value.FLASH_MAGISK -> {
MagiskInstaller.Direct(installer, outItems, logItems).exec()
if (Info.isEmulator)
MagiskInstaller.Emulator(outItems, logItems).exec()
else
MagiskInstaller.Direct(outItems, logItems).exec()
}
Const.Value.FLASH_INACTIVE_SLOT -> {
MagiskInstaller.SecondSlot(installer, outItems, logItems).exec()
MagiskInstaller.SecondSlot(outItems, logItems).exec()
}
Const.Value.PATCH_FILE -> {
uri ?: return@launch
showReboot = false
MagiskInstaller.Patch(installer, uri, outItems, logItems).exec()
MagiskInstaller.Patch(uri, outItems, logItems).exec()
}
else -> {
back()
@@ -92,9 +91,9 @@ class FlashViewModel(
private fun onResult(success: Boolean) {
state = if (success) State.LOADED else State.LOADING_FAILED
behaviorText = when {
success -> resources.getString(R.string.done)
else -> resources.getString(R.string.failure)
when {
success -> _subtitle.postValue(R.string.done)
else -> _subtitle.postValue(R.string.failure)
}
}
@@ -106,18 +105,16 @@ class FlashViewModel(
}
private fun savePressed() = withExternalRW {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
val file = MediaStoreUtils.getFile(name)
file.uri.outputStream().bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
viewModelScope.launch(Dispatchers.IO) {
val name = "magisk_install_log_%s.log".format(now.toTime(timeFormatStandard))
val file = MediaStoreUtils.getFile(name, true)
file.uri.outputStream().bufferedWriter().use { writer ->
logItems.forEach {
writer.write(it)
writer.newLine()
}
SnackbarEvent(file.toString()).publish()
}
SnackbarEvent(file.toString()).publish()
}
}

View File

@@ -1,47 +0,0 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.ktx.getLabel
class HideTarget(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
class HideAppInfo(info: ApplicationInfo, pm: PackageManager)
: ApplicationInfo(info), Comparable<HideAppInfo> {
val label = info.getLabel(pm)
val iconImage: Drawable = info.loadIcon(pm)
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<HideAppInfo>(
{ it.label.toLowerCase(currentLocale) },
{ it.packageName }
)
}
}
data class HideProcessInfo(
val name: String,
val packageName: String,
val isHidden: Boolean
)
class HideAppTarget(
val info: HideAppInfo,
val processes: List<HideProcessInfo>
) : Comparable<HideAppTarget> {
override fun compareTo(other: HideAppTarget) = compareValuesBy(this, other) { it.info }
}

View File

@@ -12,6 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ktx.hideKeyboard
import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -49,6 +52,21 @@ class HideFragment : BaseUIFragment<HideViewModel, FragmentHideMd2Binding>() {
}
})
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.hideContent.addVerticalPadding(
l_50,
l1 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
)
binding.hideContent.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.hideContent.fixEdgeEffect()
val lama = binding.hideContent.layoutManager ?: return
lama.isAutoMeasureEnabled = false
}

View File

@@ -0,0 +1,105 @@
package com.topjohnwu.magisk.ui.hide
import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.*
import android.content.pm.ServiceInfo
import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.core.utils.currentLocale
import com.topjohnwu.magisk.ktx.getLabel
import com.topjohnwu.magisk.ktx.isIsolated
import com.topjohnwu.magisk.ktx.useAppZygote
class CmdlineHiddenItem(line: String) {
val packageName: String
val process: String
init {
val split = line.split(Regex("\\|"), 2)
packageName = split[0]
process = split.getOrElse(1) { packageName }
}
}
const val ISOLATED_MAGIC = "isolated"
@SuppressLint("InlinedApi")
class HideAppInfo(info: ApplicationInfo, pm: PackageManager, hideList: List<CmdlineHiddenItem>)
: ApplicationInfo(info), Comparable<HideAppInfo> {
val label = info.getLabel(pm)
val iconImage: Drawable = info.loadIcon(pm)
val processes = fetchProcesses(pm, hideList)
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
private fun fetchProcesses(
pm: PackageManager,
hideList: List<CmdlineHiddenItem>
): List<HideProcessInfo> {
// Fetch full PackageInfo
val baseFlag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES
val packageInfo = try {
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
pm.getPackageInfo(packageName, baseFlag or request)
} catch (e: NameNotFoundException) {
// EdXposed hooked, issue#3276
return emptyList()
} catch (e: Exception) {
// Exceed binder data transfer limit, fetch each component type separately
pm.getPackageInfo(packageName, baseFlag).apply {
runCatching { activities = pm.getPackageInfo(packageName, baseFlag or GET_ACTIVITIES).activities }
runCatching { services = pm.getPackageInfo(packageName, baseFlag or GET_SERVICES).services }
runCatching { receivers = pm.getPackageInfo(packageName, baseFlag or GET_RECEIVERS).receivers }
runCatching { providers = pm.getPackageInfo(packageName, baseFlag or GET_PROVIDERS).providers }
}
}
val hidden = hideList.filter { it.packageName == packageName || it.packageName == ISOLATED_MAGIC }
fun createProcess(name: String, pkg: String = packageName): HideProcessInfo {
return HideProcessInfo(name, pkg, hidden.any { it.process == name })
}
var haveAppZygote = false
fun Array<out ComponentInfo>.processes() = map { createProcess(it.processName) }
fun Array<ServiceInfo>.processes() = map {
if (it.isIsolated) {
if (it.useAppZygote) {
haveAppZygote = true
// Using app zygote, don't need to track the process
null
} else {
createProcess("${it.processName}:${it.name}", ISOLATED_MAGIC)
}
} else {
createProcess(it.processName)
}
}
return with(packageInfo) {
activities?.processes().orEmpty() +
services?.processes().orEmpty() +
receivers?.processes().orEmpty() +
providers?.processes().orEmpty() +
listOf(if (haveAppZygote) createProcess("${processName}_zygote") else null)
}.filterNotNull().distinctBy { it.name }.sortedBy { it.name }
}
companion object {
private val comparator = compareBy<HideAppInfo>(
{ it.label.toLowerCase(currentLocale) },
{ it.packageName }
)
}
}
data class HideProcessInfo(
val name: String,
val packageName: String,
var isHidden: Boolean
) {
val isIsolated get() = name == ISOLATED_MAGIC
val isAppZygote get() = name.endsWith("_zygote")
}

View File

@@ -12,14 +12,13 @@ import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell
import kotlin.math.roundToInt
class HideItem(
app: HideAppTarget
) : ObservableItem<HideItem>(), Comparable<HideItem> {
class HideRvItem(
val info: HideAppInfo
) : ObservableItem<HideRvItem>(), Comparable<HideRvItem> {
override val layoutRes = R.layout.item_hide_md2
override val layoutRes get() = R.layout.item_hide_md2
val info = app.info
val processes = app.processes.map { HideProcessItem(it) }
val processes = info.processes.map { HideProcessRvItem(it) }
@get:Bindable
var isExpanded = false
@@ -39,14 +38,18 @@ class HideItem(
get() = state
set(value) = set(value, state, { state = it }, BR.hiddenState) {
if (value == true) {
processes.filterNot { it.isHidden }
processes
.filterNot { it.isHidden }
.filter { isExpanded || it.defaultSelection }
} else {
processes
.filter { it.isHidden }
}.forEach { it.toggle() }
}
init {
processes.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } }
addOnPropertyChangedCallback(BR.expanded) { recalculateChecked() }
recalculateChecked()
}
@@ -57,17 +60,26 @@ class HideItem(
private fun recalculateChecked() {
itemsChecked = processes.count { it.isHidden }
state = when (itemsChecked) {
0 -> false
processes.size -> true
else -> null
state = if (isExpanded) {
when (itemsChecked) {
0 -> false
processes.size -> true
else -> null
}
} else {
val defaultProcesses = processes.filter { it.defaultSelection }
when (defaultProcesses.count { it.isHidden }) {
0 -> false
defaultProcesses.size -> true
else -> null
}
}
}
override fun compareTo(other: HideItem) = comparator.compare(this, other)
override fun compareTo(other: HideRvItem) = comparator.compare(this, other)
companion object {
private val comparator = compareBy<HideItem>(
private val comparator = compareBy<HideRvItem>(
{ it.itemsChecked == 0 },
{ it.info }
)
@@ -75,16 +87,17 @@ class HideItem(
}
class HideProcessItem(
class HideProcessRvItem(
val process: HideProcessInfo
) : ObservableItem<HideProcessItem>() {
) : ObservableItem<HideProcessRvItem>() {
override val layoutRes = R.layout.item_hide_process_md2
override val layoutRes get() = R.layout.item_hide_process_md2
@get:Bindable
var isHidden = process.isHidden
set(value) = set(value, field, { field = it }, BR.hidden) {
val arg = if (isHidden) "add" else "rm"
var isHidden
get() = process.isHidden
set(value) = set(value, process.isHidden, { process.isHidden = it }, BR.hidden) {
val arg = if (it) "add" else "rm"
val (name, pkg) = process
Shell.su("magiskhide --$arg $pkg $name").submit()
}
@@ -93,7 +106,10 @@ class HideProcessItem(
isHidden = !isHidden
}
override fun contentSameAs(other: HideProcessItem) = process == other.process
override fun itemSameAs(other: HideProcessItem) = process.name == other.process.name
val defaultSelection get() =
process.isIsolated || process.isAppZygote || process.name == process.packageName
override fun contentSameAs(other: HideProcessRvItem) = process == other.process
override fun itemSameAs(other: HideProcessRvItem) = process.name == other.process.name
}

View File

@@ -1,7 +1,10 @@
package com.topjohnwu.magisk.ui.hide
import android.annotation.SuppressLint
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
import android.os.Process
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR
@@ -11,9 +14,7 @@ import com.topjohnwu.magisk.arch.filterableListOf
import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ktx.packageInfo
import com.topjohnwu.magisk.ktx.packageName
import com.topjohnwu.magisk.ktx.processes
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell
@@ -27,25 +28,32 @@ class HideViewModel : BaseViewModel(), Queryable {
@get:Bindable
var isShowSystem = Config.showSystemApp
set(value) = set(value, field, { field = it }, BR.showSystem){
set(value) = set(value, field, { field = it }, BR.showSystem) {
Config.showSystemApp = it
submitQuery()
}
@get:Bindable
var query = ""
set(value) = set(value, field, { field = it }, BR.query){
var isShowOS = false
set(value) = set(value, field, { field = it }, BR.showOS) {
submitQuery()
}
val items = filterableListOf<HideItem>()
val itemBinding = itemBindingOf<HideItem> {
@get:Bindable
var query = ""
set(value) = set(value, field, { field = it }, BR.query) {
submitQuery()
}
val items = filterableListOf<HideRvItem>()
val itemBinding = itemBindingOf<HideRvItem> {
it.bindExtra(BR.viewModel, this)
}
val itemInternalBinding = itemBindingOf<HideProcessItem> {
val itemInternalBinding = itemBindingOf<HideProcessRvItem> {
it.bindExtra(BR.viewModel, this)
}
@SuppressLint("InlinedApi")
override fun refresh() = viewModelScope.launch {
if (!Utils.showSuperUser()) {
state = State.LOADING_FAILED
@@ -54,14 +62,13 @@ class HideViewModel : BaseViewModel(), Queryable {
state = State.LOADING
val (apps, diff) = withContext(Dispatchers.Default) {
val pm = get<PackageManager>()
val hides = Shell.su("magiskhide --ls").exec().out.map { HideTarget(it) }
val apps = pm.getInstalledApplications(0)
val hideList = Shell.su("magiskhide --ls").exec().out.map { CmdlineHiddenItem(it) }
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES)
.asSequence()
.filter { it.enabled && it.uid >= 10000 && !blacklist.contains(it.packageName) }
.map { HideAppInfo(it, pm) }
.map { createTarget(it, hides) }
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map { HideAppInfo(it, pm, hideList) }
.filter { it.processes.isNotEmpty() }
.map { HideItem(it) }
.map { HideRvItem(it) }
.toList()
.sorted()
apps to items.calculateDiff(apps)
@@ -72,24 +79,18 @@ class HideViewModel : BaseViewModel(), Queryable {
// ---
private fun createTarget(info: HideAppInfo, hideList: List<HideTarget>): HideAppTarget {
val pkg = info.packageName
val hidden = hideList.filter { it.packageName == pkg }
val processNames = info.packageInfo.processes.distinct()
val processes = processNames.map { name ->
HideProcessInfo(name, pkg, hidden.any { name == it.process })
}
return HideAppTarget(info, processes)
}
// ---
override fun query() {
items.filter {
fun showHidden() = it.itemsChecked != 0
fun filterSystem() =
isShowSystem || it.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
fun filterSystem() = isShowSystem || it.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
fun isApp(uid: Int) = run {
val appId: Int = uid % 100000
appId >= Process.FIRST_APPLICATION_UID && appId <= Process.LAST_APPLICATION_UID
}
fun filterOS() = (isShowSystem && isShowOS) || isApp(it.info.uid)
fun filterQuery(): Boolean {
fun inName() = it.info.label.contains(query, true)
@@ -98,7 +99,7 @@ class HideViewModel : BaseViewModel(), Queryable {
return inName() || inPackage() || inProcesses()
}
showHidden() || (filterSystem() && filterQuery())
showHidden() || (filterSystem() && filterOS() && filterQuery())
}
state = State.LOADED
}
@@ -121,4 +122,3 @@ class HideViewModel : BaseViewModel(), Queryable {
) }
}
}

View File

@@ -57,7 +57,9 @@ sealed class IconLink : RvItem() {
object App : PayPal(), AppDev
object Main : PayPal(), MainDev
object Main : PayPal() {
override val name: String get() = "magiskdonate"
}
}
object Patreon : IconLink() {

View File

@@ -2,6 +2,8 @@ package com.topjohnwu.magisk.ui.home
import android.os.Bundle
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloader
@@ -22,20 +24,32 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
private fun checkTitle(text: TextView, icon: ImageView) {
text.post {
if (text.layout.getEllipsisCount(0) != 0) {
with (icon) {
layoutParams.width = 0
layoutParams.height = 0
requestLayout()
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
): View {
super.onCreateView(inflater, container, savedInstanceState)
// Set barrier reference IDs in code, since resource IDs will be stripped in release mode
binding.homeMagiskWrapper.homeMagiskTitleBarrier.referencedIds =
intArrayOf(R.id.home_magisk_button, R.id.home_magisk_title, R.id.home_magisk_icon)
binding.homeMagiskWrapper.homeMagiskBarrier.referencedIds =
intArrayOf(R.id.home_magisk_latest_version, R.id.home_magisk_installed_version)
binding.homeManagerWrapper.homeManagerTitleBarrier.referencedIds =
intArrayOf(R.id.home_manager_button, R.id.home_manager_title, R.id.home_manager_icon)
// If titles are squished, hide icons
with(binding.homeMagiskWrapper) {
checkTitle(homeMagiskTitle, homeMagiskIcon)
}
with(binding.homeManagerWrapper) {
checkTitle(homeManagerTitle, homeManagerIcon)
}
return binding.root
}
@@ -46,11 +60,14 @@ class HomeFragment : BaseUIFragment<HomeViewModel, FragmentHomeMd2Binding>() {
menu.removeItem(R.id.action_reboot)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_settings -> HomeFragmentDirections.actionHomeFragmentToSettingsFragment()
.navigate()
R.id.action_reboot -> RebootEvent.inflateMenu(activity).show()
else -> null
}?.let { true } ?: super.onOptionsItemSelected(item)
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_settings ->
HomeFragmentDirections.actionHomeFragmentToSettingsFragment().navigate()
R.id.action_reboot -> RebootEvent.inflateMenu(activity).show()
else -> return super.onOptionsItemSelected(item)
}
return true
}
}

View File

@@ -1,6 +1,5 @@
package com.topjohnwu.magisk.ui.home
import android.os.Build
import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BuildConfig
@@ -10,17 +9,14 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.download.Subject.Manager
import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.events.OpenInappLinkEvent
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.EnvFixDialog
import com.topjohnwu.magisk.events.dialog.ManagerInstallDialog
import com.topjohnwu.magisk.events.dialog.UninstallDialog
import com.topjohnwu.magisk.ktx.await
import com.topjohnwu.magisk.ktx.packageName
import com.topjohnwu.magisk.ktx.res
import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch
@@ -32,45 +28,49 @@ enum class MagiskState {
}
class HomeViewModel(
private val repoMagisk: MagiskRepository
private val svc: NetworkService
) : BaseViewModel() {
val magiskTitleBarrierIds =
intArrayOf(R.id.home_magisk_icon, R.id.home_magisk_title, R.id.home_magisk_button)
val magiskDetailBarrierIds =
intArrayOf(R.id.home_magisk_installed_version, R.id.home_device_details_ramdisk)
val appTitleBarrierIds =
intArrayOf(R.id.home_manager_icon, R.id.home_manager_title, R.id.home_manager_button)
@get:Bindable
var isNoticeVisible = Config.safetyNotice
set(value) = set(value, field, { field = it }, BR.noticeVisible)
@get:Bindable
var stateMagisk = MagiskState.LOADING
set(value) = set(value, field, { field = it }, BR.stateMagisk, BR.showUninstall)
val stateMagisk = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED
Info.env.magiskVersionCode < BuildConfig.VERSION_CODE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
@get:Bindable
var stateManager = MagiskState.LOADING
set(value) = set(value, field, { field = it }, BR.stateManager)
@get:Bindable
var magiskRemoteVersion = R.string.loading.res()
set(value) = set(value, field, { field = it }, BR.magiskRemoteVersion)
val magiskInstalledVersion get() =
"${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})"
val magiskInstalledVersion get() = Info.env.run {
if (isActive)
"$magiskVersionString ($magiskVersionCode)".asTransitive()
else
R.string.not_available.asTransitive()
}
@get:Bindable
var managerRemoteVersion = R.string.loading.res()
var managerRemoteVersion = R.string.loading.asTransitive()
set(value) = set(value, field, { field = it }, BR.managerRemoteVersion)
val managerInstalledVersion = Info.stub?.let {
"${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) (${it.version})"
} ?: "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})"
val statePackageName = packageName
@get:Bindable
var stateManagerProgress = 0
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
@get:Bindable
val showUninstall get() =
Info.env.magiskVersionCode > 0 && stateMagisk != MagiskState.LOADING && isConnected.get()
@get:Bindable
val showSafetyNet get() = Info.hasGMS && isConnected.get()
@@ -82,31 +82,25 @@ class HomeViewModel(
override fun refresh() = viewModelScope.launch {
state = State.LOADING
notifyPropertyChanged(BR.showUninstall)
notifyPropertyChanged(BR.showSafetyNet)
repoMagisk.fetchUpdate()?.apply {
Info.getRemote(svc)?.apply {
state = State.LOADED
stateMagisk = when {
!Info.env.isActive -> MagiskState.NOT_INSTALLED
magisk.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
stateManager = when {
!app.isUpdateChannelCorrect && isConnected.get() -> MagiskState.NOT_INSTALLED
app.isObsolete -> MagiskState.OBSOLETE
BuildConfig.VERSION_CODE < magisk.versionCode -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
magiskRemoteVersion =
"${magisk.version} (${magisk.versionCode})"
managerRemoteVersion =
"${app.version} (${app.versionCode}) (${stub.versionCode})"
"${magisk.version} (${magisk.versionCode}) (${stub.versionCode})".asTransitive()
launch {
ensureEnv()
}
} ?: apply { state = State.LOADING_FAILED }
} ?: {
state = State.LOADING_FAILED
managerRemoteVersion = R.string.not_available.asTransitive()
}()
}
val showTest = false
@@ -118,23 +112,22 @@ class HomeViewModel(
}.publish()
fun onProgressUpdate(progress: Float, subject: Subject) {
when (subject) {
is Manager -> stateManagerProgress = progress.times(100f).roundToInt()
}
if (subject is Manager)
stateManagerProgress = progress.times(100f).roundToInt()
}
fun onLinkPressed(link: String) = OpenInappLinkEvent(link).publish()
fun onDeletePressed() = UninstallDialog().publish()
fun onManagerPressed() =
if (isConnected.get()) ManagerInstallDialog().publish()
else SnackbarEvent(R.string.no_connection).publish()
fun onManagerPressed() = when (state) {
State.LOADED -> withExternalRW { ManagerInstallDialog().publish() }
State.LOADING -> SnackbarEvent(R.string.loading).publish()
else -> SnackbarEvent(R.string.no_connection).publish()
}
fun onMagiskPressed() = if (isConnected.get()) withExternalRW {
fun onMagiskPressed() = withExternalRW {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish()
} else {
SnackbarEvent(R.string.no_connection).publish()
}
fun onSafetyNetPressed() =
@@ -150,17 +143,7 @@ class HomeViewModel(
MagiskState.NOT_INSTALLED,
MagiskState.LOADING
)
// Don't bother checking env when magisk is not installed, loading or already has been shown
if (
invalidStates.any { it == stateMagisk } ||
shownDialog ||
// don't care for emulators either
Build.DEVICE.orEmpty().contains("generic") ||
Build.PRODUCT.orEmpty().contains("generic")
) {
return
}
if (invalidStates.any { it == stateMagisk } || shownDialog) return
val result = Shell.su("env_check").await()
if (!result.isSuccess) {
@@ -169,11 +152,4 @@ class HomeViewModel(
}
}
private val MagiskJson.isObsolete
get() = Info.env.isActive && Info.env.magiskVersionCode < versionCode
private val ManagerJson.isUpdateChannelCorrect
get() = versionCode > 0
private val ManagerJson.isObsolete
get() = BuildConfig.VERSION_CODE < versionCode
}

View File

@@ -0,0 +1,88 @@
package com.topjohnwu.magisk.ui.inflater
import android.content.Context
import android.os.Build
import android.util.AttributeSet
import android.view.InflateException
import android.view.LayoutInflater
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SimpleArrayMap
import java.lang.reflect.Constructor
open class LayoutInflaterFactory(private val delegate: AppCompatDelegate) : LayoutInflater.Factory2 {
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
return onCreateView(null, name, context, attrs)
}
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
val view = delegate.createView(parent, name, context, attrs)
?: LayoutInflaterFactoryDefaultImpl.createViewFromTag(context, name, attrs)
onViewCreated(view, parent, name, context, attrs)
return view
}
open fun onViewCreated(view: View?, parent: View?, name: String, context: Context, attrs: AttributeSet) {
if (view == null) return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
WindowInsetsHelper.attach(view, attrs)
}
}
}
private object LayoutInflaterFactoryDefaultImpl {
private val constructorSignature = arrayOf(
Context::class.java, AttributeSet::class.java)
private val classPrefixList = arrayOf(
"android.widget.",
"android.view.",
"android.webkit."
)
private val constructorMap = SimpleArrayMap<String, Constructor<out View?>>()
fun createViewFromTag(context: Context, name: String, attrs: AttributeSet): View? {
var name = name
if (name == "view") {
name = attrs.getAttributeValue(null, "class")
}
return try {
if (-1 == name.indexOf('.')) {
for (prefix in classPrefixList) {
val view: View? = createViewByPrefix(context, name, attrs, prefix)
if (view != null) {
return view
}
}
null
} else {
createViewByPrefix(context, name, attrs, null)
}
} catch (e: Exception) {
null
}
}
@Throws(ClassNotFoundException::class, InflateException::class)
private fun createViewByPrefix(context: Context, name: String, attrs: AttributeSet, prefix: String?): View? {
var constructor = constructorMap[name]
return try {
if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it
val clazz = Class.forName(
if (prefix != null) prefix + name else name,
false,
context.classLoader).asSubclass(View::class.java)
constructor = clazz.getConstructor(*constructorSignature)
constructorMap.put(name, constructor)
}
constructor!!.isAccessible = true
constructor.newInstance(context, attrs)
} catch (e: Exception) {
null
}
}
}

View File

@@ -0,0 +1,284 @@
@file:Suppress("unused")
package com.topjohnwu.magisk.ui.inflater
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.graphics.Rect
import android.os.Build
import android.util.AttributeSet
import android.view.Gravity.*
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.Insets
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import com.topjohnwu.magisk.R
private typealias ApplyInsetsCallback<T> = (insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) -> T
private class ApplyInsets(private val out: Rect) : ApplyInsetsCallback<Unit> {
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) {
out.left += if (left) insets.left else 0
out.top += if (top) insets.top else 0
out.right += if (right) insets.right else 0
out.bottom += if (bottom) insets.bottom else 0
}
}
private class ConsumeInsets : ApplyInsetsCallback<Insets> {
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean): Insets {
val insetsLeft = if (left) 0 else insets.left
val insetsTop = if (top) 0 else insets.top
val insetsRight = if (right) 0 else insets.right
val insetsBottom = if (bottom) 0 else insets.bottom
return Insets.of(insetsLeft, insetsTop, insetsRight, insetsBottom)
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
open class WindowInsetsHelper private constructor(
private val view: View,
private val fitSystemWindows: Int,
private val layout_fitsSystemWindowsInsets: Int,
private val consumeSystemWindows: Int) : OnApplyWindowInsetsListener {
internal var initialPaddingLeft: Int = view.paddingLeft
internal var initialPaddingTop: Int = view.paddingTop
internal var initialPaddingRight: Int = view.paddingRight
internal var initialPaddingBottom: Int = view.paddingBottom
private var initialMargin = false
internal var initialMarginLeft: Int = 0
internal var initialMarginTop: Int = 0
internal var initialMarginRight: Int = 0
internal var initialMarginBottom: Int = 0
internal var initialMarginStart: Int = 0
internal var initialMarginEnd: Int = 0
private var lastInsets: WindowInsetsCompat? = null
open fun setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
initialPaddingLeft = left
initialPaddingTop = top
initialPaddingRight = right
initialPaddingBottom = bottom
lastInsets?.let { applyWindowInsets(it) }
}
open fun setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
if (isRTL) {
setInitialPadding(start, top, end, bottom)
} else {
setInitialPadding(start, top, end, bottom)
}
}
open fun setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
initialPaddingLeft = left
initialPaddingTop = top
initialPaddingRight = right
initialPaddingBottom = bottom
lastInsets?.let { applyWindowInsets(it) }
}
open fun setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
initialMarginStart = start
initialMarginTop = top
initialMarginEnd = end
initialMarginBottom = bottom
lastInsets?.let { applyWindowInsets(it) }
}
@SuppressLint("RtlHardcoded")
private fun <T> applyInsets(insets: Insets, fit: Int, callback: ApplyInsetsCallback<T>): T {
val relativeMode = (fit and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
val left: Boolean
val top = fit and TOP == TOP
val right: Boolean
val bottom = fit and BOTTOM == BOTTOM
if (relativeMode) {
val start = fit and START == START
val end = fit and END == END
left = (!isRTL && start) || (isRTL && end)
right = (!isRTL && end) || (isRTL && start)
} else {
left = fit and LEFT == LEFT
right = fit and RIGHT == RIGHT
}
return callback.invoke(insets, left, top, right, bottom)
}
private fun applyWindowInsets(windowInsets: WindowInsetsCompat): WindowInsetsCompat {
if (fitSystemWindows != 0) {
val padding = Rect(initialPaddingLeft, initialPaddingTop, initialPaddingRight, initialPaddingBottom)
applyInsets(windowInsets.systemWindowInsets, fitSystemWindows, ApplyInsets(padding))
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)
}
if (layout_fitsSystemWindowsInsets != 0) {
if (!initialMargin) {
initialMarginLeft = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.leftMargin ?: 0
initialMarginTop = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0
initialMarginRight = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.rightMargin ?: 0
initialMarginBottom = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0
initialMarginStart = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart ?: 0
initialMarginEnd = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginEnd ?: 0
initialMargin = true
}
val margin = if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION)
Rect(initialMarginLeft, initialMarginTop, initialMarginRight, initialMarginBottom)
else
Rect(initialMarginStart, initialMarginTop, initialMarginEnd, initialMarginBottom)
applyInsets(windowInsets.systemWindowInsets, layout_fitsSystemWindowsInsets, ApplyInsets(margin))
val lp = view.layoutParams
if (lp is ViewGroup.MarginLayoutParams) {
lp.topMargin = margin.top
lp.bottomMargin = margin.bottom
if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION) {
lp.marginStart = margin.left
lp.marginEnd = margin.right
} else {
lp.leftMargin = margin.left
lp.rightMargin = margin.right
}
view.layoutParams = lp
}
}
val systemWindowInsets = if (consumeSystemWindows != 0) applyInsets(windowInsets.systemWindowInsets, consumeSystemWindows, ConsumeInsets()) else windowInsets.systemWindowInsets
return WindowInsetsCompat.Builder(windowInsets)
.setSystemWindowInsets(systemWindowInsets)
.build()
}
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
if (lastInsets == insets) {
return insets
}
lastInsets = insets
return applyWindowInsets(insets)
}
companion object {
@JvmStatic
fun attach(view: View, attrs: AttributeSet) {
val a = view.context.obtainStyledAttributes(attrs, R.styleable.WindowInsetsHelper, 0, 0)
val edgeToEdge = a.getBoolean(R.styleable.WindowInsetsHelper_edgeToEdge, false)
val fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_fitsSystemWindowsInsets, 0)
val layout_fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_layout_fitsSystemWindowsInsets, 0)
val consumeSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_consumeSystemWindowsInsets, 0)
a.recycle()
attach(view, edgeToEdge, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
}
@JvmStatic
fun attach(view: View, edgeToEdge: Boolean, fitsSystemWindowsInsets: Int, layout_fitsSystemWindowsInsets: Int, consumeSystemWindowsInsets: Int) {
if (edgeToEdge) {
view.systemUiVisibility = (view.systemUiVisibility
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
if (fitsSystemWindowsInsets == 0 && layout_fitsSystemWindowsInsets == 0 && consumeSystemWindowsInsets == 0) {
return
}
val listener = WindowInsetsHelper(view, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
ViewCompat.setOnApplyWindowInsetsListener(view, listener)
view.setTag(R.id.tag_rikka_material_WindowInsetsHelper, listener)
if (!view.isAttachedToWindow) {
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
}
}
val View.windowInsetsHelper: WindowInsetsHelper?
get() {
val value = getTag(R.id.tag_rikka_material_WindowInsetsHelper)
return if (value is WindowInsetsHelper) value else null
}
val View.initialPaddingLeft: Int
get() = windowInsetsHelper?.initialPaddingLeft ?: 0
val View.initialPaddingTop: Int
get() = windowInsetsHelper?.initialPaddingTop ?: 0
val View.initialPaddingRight: Int
get() = windowInsetsHelper?.initialPaddingRight ?: 0
val View.initialPaddingBottom: Int
get() = windowInsetsHelper?.initialPaddingBottom ?: 0
val View.initialPaddingStart: Int
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingRight else initialPaddingLeft
val View.initialPaddingEnd: Int
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingLeft else initialPaddingRight
fun View.setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
windowInsetsHelper?.setInitialPadding(left, top, right, bottom)
}
fun View.setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
windowInsetsHelper?.setInitialPaddingRelative(start, top, end, bottom)
}
val View.initialMarginLeft: Int
get() = windowInsetsHelper?.initialMarginLeft ?: 0
val View.initialMarginTop: Int
get() = windowInsetsHelper?.initialMarginTop ?: 0
val View.initialMarginRight: Int
get() = windowInsetsHelper?.initialMarginRight ?: 0
val View.initialMarginBottom: Int
get() = windowInsetsHelper?.initialMarginBottom ?: 0
val View.initialMarginStart: Int
get() = windowInsetsHelper?.initialMarginStart ?: 0
val View.initialMarginEnd: Int
get() = windowInsetsHelper?.initialMarginEnd ?: 0
fun View.setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
windowInsetsHelper?.setInitialMargin(left, top, right, bottom)
}
fun View.setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
windowInsetsHelper?.setInitialMarginRelative(start, top, end, bottom)
}

View File

@@ -1,12 +1,13 @@
package com.topjohnwu.magisk.ui.install
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentInstallMd2Binding
import com.topjohnwu.magisk.events.RequestFileEvent
import com.topjohnwu.magisk.ktx.coroutineScope
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -15,18 +16,28 @@ class InstallFragment : BaseUIFragment<InstallViewModel, FragmentInstallMd2Bindi
override val layoutRes = R.layout.fragment_install_md2
override val viewModel by viewModel<InstallViewModel>()
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
viewModel.data = RequestFileEvent.resolve(requestCode, resultCode, data)
}
override fun onStart() {
super.onStart()
requireActivity().setTitle(R.string.install)
// Allow markwon to run in viewmodel scope
binding.releaseNotes.coroutineScope = viewModel.viewModelScope
BaseDownloader.observeProgress(this, viewModel::onProgressUpdate)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel._method = savedInstanceState?.getInt(KEY_CURRENT_METHOD, -1) ?: -1
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(KEY_CURRENT_METHOD, viewModel.method)
}
companion object {
private const val KEY_CURRENT_METHOD = "current_method"
}
}

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