Compare commits

...

316 Commits
v19.3 ... v20.0

Author SHA1 Message Date
topjohnwu
59fd38bbf8 Add v7.3.5 changelog 2019-10-11 16:12:32 -04:00
topjohnwu
06dc6df270 Allow dalvik runtime to load snet 2019-10-11 03:58:04 -04:00
topjohnwu
ff8460b361 Update dependencies 2019-10-11 03:29:55 -04:00
topjohnwu
674d272eaa Support pre-5.0 without GMS
Fix #1912
2019-10-11 01:46:15 -04:00
topjohnwu
c3e00c279d Legacy adb shell does not have uname 2019-10-11 01:45:06 -04:00
dark-basic
175d920c94 Update strings.xml
I'M BACK.
New translations were added.
2019-10-10 17:17:09 -04:00
topjohnwu
04920883ea Change code for handling tar files 2019-10-10 15:07:45 -04:00
topjohnwu
5e44b0b9d5 Use raw literals for scripts 2019-10-09 17:38:45 -04:00
topjohnwu
23c1a1dab8 Some code reorganizing 2019-10-09 16:01:21 -04:00
topjohnwu
f5d054b93c Add support for PXA DTBs 2019-10-08 23:49:21 -04:00
topjohnwu
d25ae5e0a9 Add __attribute__((packed)) just in case 2019-10-08 16:55:25 -04:00
topjohnwu
c42a51dcbb Add support to patch DTBH DTBs
Apparently, Qualcomm is not the only on creating weird DTB formats,
Samsung also have their own DTBH format for Exynos platforms.

Close #1902
2019-10-08 16:43:27 -04:00
topjohnwu
da3fd92b31 Prevent unsigned overflow
Close #1898
2019-10-08 15:55:27 -04:00
topjohnwu
4a45ba3c14 Update magisk_files commit hashes 2019-10-08 14:53:04 -04:00
Madis
dbc8bed234 Estonian update 2019-10-07 23:04:19 -04:00
Gaurav
f8b4190a11 Fix Typos 2019-10-07 23:03:09 -04:00
Mevlüt TOPÇU
479972e3ae Update Turkish language
Hi

Merge please

Thank's
2019-10-07 23:02:29 -04:00
Viktor De Pasquale
3ea28b0afb Fixed permission event not being executed 2019-10-07 22:58:14 -04:00
Viktor De Pasquale
2b3cc28966 Fixed snackbar not showing up for dumping files 2019-10-07 22:58:14 -04:00
Viktor De Pasquale
751642b39a Fixed back button not working on flash screen 2019-10-07 22:58:14 -04:00
topjohnwu
d6c2c821a4 Minor improvements in QCDT logic 2019-10-07 22:57:01 -04:00
Alessandro Astone
dfc65b95f7 qcdt: pad the last dtb too 2019-10-07 22:48:54 -04:00
Alessandro Astone
b45d922463 qcdt: include padding in the table length fields 2019-10-07 22:48:54 -04:00
topjohnwu
f87ee3fcf9 Refactor boot image unpack/repack code base 2019-10-07 04:35:02 -04:00
topjohnwu
e0927cd763 Add support to patch QCDT
Old Qualcomn devices have their own special QC table of DTB to
store device trees. Since patching fstab is now mandatory on Android 10,
and for older devices all early mount devices have to be included into
the fstab in DTBs, patching QCDT is crucial for rooting Android 10
on legacy devices.

Close #1876 (Thanks for getting me aware of this issue!)
2019-10-07 00:38:02 -04:00
topjohnwu
21099eabfa Small changes in DTB code 2019-10-05 17:24:53 -04:00
topjohnwu
abbd2e6b72 Update AS 2019-10-05 17:02:08 -04:00
topjohnwu
5b7ddbbb01 Fix status report UI 2019-09-30 15:32:28 -04:00
Viktor De Pasquale
6352fbb3b2 Added additional sorting for installed modules 2019-09-30 14:07:14 -04:00
topjohnwu
d3f49334e2 Move function as extension 2019-09-28 12:17:34 -04:00
topjohnwu
c4356171b3 Update dependencies block 2019-09-28 05:01:51 -04:00
topjohnwu
5c5625911d Fix back button behavior 2019-09-28 05:01:25 -04:00
topjohnwu
6a10cc9c55 Remove dependency Dexter 2019-09-28 04:23:21 -04:00
topjohnwu
6b317f918e Rename base class names 2019-09-28 03:50:11 -04:00
topjohnwu
08b528dc4f Reorganize classes
- Move base classes to its own package
- Move most logic out of MagiskActivity to MainActivity
2019-09-28 03:37:24 -04:00
topjohnwu
fc886a5a47 Merge Teanity into sources 2019-09-28 01:56:16 -04:00
topjohnwu
0cb90e2e55 Update BasePreferenceFragment 2019-09-27 19:54:03 -04:00
topjohnwu
64113a69b4 Remove unused warnings 2019-09-26 13:54:40 -04:00
topjohnwu
544bb7459c Don't pass by reference 2019-09-26 03:49:05 -04:00
Viktor De Pasquale
578a50b464 Added hiding actions on notifications typed "Download" 2019-09-26 03:15:46 -04:00
topjohnwu
3d4081d0af Fix patch verity and forceencrypt 2019-09-26 03:14:56 -04:00
topjohnwu
b763b81f56 Use mutex_guard to lock su_info 2019-09-26 01:49:50 -04:00
topjohnwu
947dae4900 Rename classes and small adjustments 2019-09-25 23:55:39 -04:00
topjohnwu
debd1d7d54 Update canary channel links 2019-09-24 03:09:02 -04:00
osm0sis
cba0d04000 magiskpolicy: rules: standardize update_engine sepolicy when rooted
The state of ROM A/B OTA addon.d-v2 support is an inconsistent mess currently:
- LineageOS builds userdebug with permissive update_engine domain, OmniROM builds userdebug with a more restricted update_engine domain, and CarbonROM builds user with a hybrid closer to Omni's
- addon.d-v2 scripts cannot function to the full extent they should when there is a more restricted update_engine domain sepolicy in place, which is likely why Lineage made update_engine completely permissive

Evidence for the above:
- many addon.d-v2 scripts only work (or fully work) on Lineage, see below
- Magisk's addon.d-v2 script would work on Lineage without issue, but would work on Carbon and Omni only if further allow rules were added for basic things like "file read" and "dir search" suggesting these ROMs' addon.d-v2 is severely limited
- Omni includes a /system/addon.d/69-gapps.sh script with the ROM itself (despite shipping without GApps), and with Magisk's more permissive sepolicy and no GApps installed it will remove important ROM files during OTA, resulting in a bootloop; the issue with shipping this script was therefore masked by Omni's overly restrictive update_engine sepolicy not allowing the script to function as intended

The solution:
- guarantee a consistent addon.d-v2 experience for users across ROMs when rooted with Magisk by making update_engine permissive as Lineage has
- hopefully ROMs can work together to come up with something standard for unrooted addon.d-v2 function
2019-09-23 07:55:25 -04:00
topjohnwu
695e7e6da0 Create product mirror if /system/product exist 2019-09-23 06:52:24 -04:00
topjohnwu
4cd4bfa1d7 Add ':' to allowed characters for magiskhide process name 2019-09-22 16:17:51 -04:00
topjohnwu
16b400964b Update vars for 2SI 2019-09-22 06:45:23 -04:00
topjohnwu
cf2d02c0dd Don't wipe ramdisk when A-only SAR 2019-09-22 06:17:54 -04:00
topjohnwu
0fcd0de0d1 Fix potential crash when traversing cpio entries 2019-09-22 06:15:19 -04:00
topjohnwu
748a35774f Support patching fstab in ramdisk for A-only 2SI 2019-09-22 05:30:04 -04:00
topjohnwu
a52a3e38ed Change some class names 2019-09-22 05:20:51 -04:00
topjohnwu
ee0cef06a6 Add support for A-only 2SI 2019-09-22 05:15:31 -04:00
topjohnwu
0e5a113a0c Support patching mnt_point in fstab in dtb 2019-09-22 04:17:15 -04:00
topjohnwu
a1ccd44013 Change MagiskBoot patch behavior
Use environment variables to toggle configurations for patching ramdisk
2019-09-21 05:55:23 -04:00
topjohnwu
4d91e50d6d Update dtb patch to not use in-place modification 2019-09-21 05:30:04 -04:00
topjohnwu
120668c7bc Revise dtb commands CLI 2019-09-20 03:53:58 -04:00
topjohnwu
d81ccde569 Pretty print dtb content 2019-09-20 03:05:14 -04:00
topjohnwu
e8581b4adb Fix links in docs 2019-09-19 05:48:21 -04:00
topjohnwu
19906575a3 Update v7.3.4 changelogs 2019-09-19 05:29:45 -04:00
topjohnwu
9329094a4e Update documentations 2019-09-19 05:00:29 -04:00
topjohnwu
b44f5122fd Pass int directly as pointer 2019-09-19 00:13:42 -04:00
topjohnwu
17981730a4 Remove load_persist_props in post-fs-data
Close #1607
2019-09-17 13:50:53 -04:00
topjohnwu
53de6da26c Only print relevant info according to header version 2019-09-17 05:11:09 -04:00
topjohnwu
3e30ccdeee Make parsing behaves according to header
Close #1778. Close #1848
2019-09-17 05:01:04 -04:00
topjohnwu
baaaf7d5de Fully match zygote/usap process names 2019-09-17 01:50:45 -04:00
hoijui
45d8d139a9 Cross-link-ify install instructions 2019-09-17 01:28:41 -04:00
topjohnwu
fe644e10d0 Make sure post-fs-data is first ran
Close #1601
2019-09-17 00:21:07 -04:00
impulsiva
f383d11d10 Update TR strings
Fixed several typos and mistranslated strings.
2019-09-13 16:29:25 -04:00
topjohnwu
ef1b928532 LD_LIBRARY_PATH patch for apex should not propagate
Fix #1832
2019-09-13 15:22:49 -04:00
topjohnwu
6e46d394b1 Fix su_info cache yet again... 2019-09-13 14:05:28 -04:00
topjohnwu
f109038d12 Hardcode shell uid to 2000 2019-09-13 03:14:58 -04:00
topjohnwu
e31e687602 Allow ADB shell to remove modules and reboot 2019-09-13 03:14:21 -04:00
topjohnwu
86bfb22d4c Override module when .replace is found 2019-09-12 16:08:30 -04:00
topjohnwu
3f057367e3 Update dependencies 2019-09-12 12:50:44 -04:00
topjohnwu
3d7ed5820e Update busybox
Close #1520
2019-09-11 23:06:49 -04:00
topjohnwu
0118f2efa7 Merge styles 2019-09-09 19:58:19 -04:00
topjohnwu
15312e4709 Remove unused resources 2019-09-09 17:57:25 -04:00
topjohnwu
bf1568a73a Fix strings 2019-09-09 17:43:16 -04:00
Salim B
13a2520ea5 Fix typo and add link to GH issues 2019-09-09 17:39:15 -04:00
Frieder Bluemle
f53238f206 Update Gradle wrapper to 5.6.2 2019-09-09 17:38:54 -04:00
Rom
9375748d9b Update French translation 2019-09-09 17:38:22 -04:00
Gozzwip
201df54e79 new strings added 2019-09-09 17:37:41 -04:00
vvb2060
0b54fe477b Update zh-rCN translation 2019-09-09 17:37:08 -04:00
Ilya Kushnir
4119e6669e Update RU strings 2019-09-09 17:36:57 -04:00
Taras
d33e5226b3 Update Ukrainian translation 2019-09-09 17:36:39 -04:00
topjohnwu
d73f39c706 Fix manager update after hidden
Fix #1828
2019-09-09 17:24:29 -04:00
topjohnwu
087b451e17 Fix strings 2019-09-08 01:19:33 -04:00
topjohnwu
86481c74ff Allow user to select recovery mode
Close #1674
2019-09-08 00:44:26 -04:00
topjohnwu
5b937fb1fa Random changes 2019-09-05 11:36:48 -04:00
topjohnwu
ff828116bc Only cache magisk zips 2019-09-05 11:26:35 -04:00
topjohnwu
ee39616a8b Update emulator.sh to support all AVD images 2019-09-04 11:12:09 -04:00
topjohnwu
cdb53ca049 Fix su_info cache bug 2019-09-04 11:04:59 -04:00
topjohnwu
8cf475f708 Add scripts to setup Magisk in AVD 2019-09-03 17:06:14 -04:00
topjohnwu
0cb449e1d6 We need to support pre-5.0 platforms 2019-09-03 16:28:27 -04:00
topjohnwu
e6adb7abca Make kotlin version a variable globally 2019-09-03 16:27:57 -04:00
topjohnwu
cfad7dd317 Sanitize magiskhide targets
Fix #1785
2019-09-01 14:16:12 +08:00
topjohnwu
dd35224f92 Minor adjustments to exec_sql 2019-09-01 13:58:50 +08:00
Chris Renshaw
1283590eeb scripts: prepare addon.d for recovery addon.d-v2 support
- naturally there's no `su` in recovery
- major refactor for common actions and simplicity
2019-09-01 02:19:59 +08:00
osm0sis
dca3fe396f scripts: hide expected x86 busybox error on arm
- Magisk Manager installs have busybox in the $PATH before extracting busybox from update-binary so an error from busybox ash (as sh) attempting to parse the x86 busybox like a shell script would be shown:
./bin/busybox: line 1: syntax error: unexpected "("
- this will only occur when ash tries to run a binary it can't handle, so basically only with x86 binary on an arm* device
2019-09-01 02:19:59 +08:00
cristisilaghi
8d87eae11b Update RO 2019-09-01 02:17:13 +08:00
Frieder Bluemle
fd7eaacae0 Update Gradle wrapper to 5.6.1 2019-09-01 01:17:22 +08:00
topjohnwu
fba33cbbe9 Fix strings 2019-09-01 01:15:15 +08:00
Gozzwip
950ffcd790 Translation is done 2019-09-01 01:12:15 +08:00
Gozzwip
c178299013 Translation is done 2019-09-01 01:12:08 +08:00
Rom
5d17c1f588 French translation update 2019-09-01 01:11:54 +08:00
linar10
a75c00d94e Update strings.xml 2019-09-01 01:11:40 +08:00
Albert I
cd19517414 app: l10n: Update Indonesian translations
* Update the wordings
* Delocalize "Core Only" strings
* Add 3 months worth of missing translations

Signed-off-by: Albert I <kras@raphielgang.org>
2019-09-01 01:11:33 +08:00
Gozzwip
155f39aab5 Some changes 2019-09-01 01:11:06 +08:00
Mevlüt TOPÇU
4514d0b467 Update Turkish language
Hi,

Merge please

Thanks.
2019-09-01 01:10:51 +08:00
zertyuiop
6f4a938a31 Added missing strings 2019-09-01 01:10:33 +08:00
VergeDX
1303ea95dd Update & Fix Chinese Translate. 2019-09-01 01:10:19 +08:00
Oliver Cervera
727fe1bd15 Update Italian translation
Added new strings
2019-09-01 01:10:10 +08:00
topjohnwu
64ebc977e9 Small magic mount adjustments 2019-08-31 21:53:47 +08:00
topjohnwu
e89c50d934 Support /system/product wihtout /product
Fix #1676
2019-08-29 22:56:34 +08:00
topjohnwu
c859ddfb8f Upgrade Kotlin 2019-08-27 02:30:10 +08:00
topjohnwu
a6126c5eda Cosmetic changes 2019-08-23 03:05:41 +08:00
topjohnwu
85d9bd9106 Fix compile errors 2019-08-23 00:30:21 +08:00
Viktor De Pasquale
39e9622205 Fixed magisk version
Added refreshing versions before and after the request to remote
2019-08-22 08:03:17 +02:00
topjohnwu
021994c9f3 Clean elf after building shared binaries 2019-08-22 02:51:17 +08:00
topjohnwu
2e7ce2a769 Update gradle files 2019-08-21 10:38:09 +08:00
topjohnwu
84f0ff2fad Fix manager package name database management 2019-08-12 03:31:59 -07:00
topjohnwu
e6561e5f84 Fix XML parsing Kotlin error 2019-08-12 03:14:51 -07:00
topjohnwu
5fa452aa74 Multiple minor changes 2019-08-12 01:54:33 -07:00
topjohnwu
2225ccb146 Flush settings to persistent storage 2019-08-12 00:05:19 -07:00
topjohnwu
5aafc78847 Cleanup const 2019-08-11 23:53:43 -07:00
topjohnwu
0d03833cff Name module zips with version code 2019-08-11 22:46:39 -07:00
topjohnwu
a797d5d396 Update snet extension 2019-08-08 04:18:32 -07:00
topjohnwu
f2494374f8 Eliminate any traces of Java in app 2019-08-08 00:59:23 -07:00
topjohnwu
48395ba860 Remove unused files 2019-08-08 00:29:27 -07:00
topjohnwu
5ba5f5f94e Observe network connnectivity
Observe internet connectivity will ping google.com
2019-08-07 22:26:44 -07:00
topjohnwu
42ce6fd334 Workaround stupid Moshi proguard rules 2019-08-07 22:26:25 -07:00
Viktor De Pasquale
f5c3ee3ae1 Added elements of UI to "hide list" 2019-08-07 03:07:18 -07:00
Viktor De Pasquale
3c7ece1605 Fixed not showing current version
Current version was not displaying under circumstances that involve loss of connection. Versions are displayed whether the device is connected or not.
2019-08-07 03:07:18 -07:00
Viktor De Pasquale
870efc49ea Fixed using mapping function incorrectly 2019-08-07 03:07:18 -07:00
Viktor De Pasquale
085ede6d93 Added simple ui blocks for whenever connection drops out 2019-08-07 03:07:18 -07:00
Viktor De Pasquale
4ef19d17da Added a flag for connection status
Reactively updated flag which only checks whether the "data" / "wifi" / "ethernet" is plugged in or enabled. If the user connects to the wifi but has no actual connection, the app will never know.
Please refrain from using other access methods (like pinging a host), it can get picked up by a VPN or other methods and possibly expose MM.
2019-08-07 03:07:18 -07:00
topjohnwu
223913c30a Remove unnecessary App usage 2019-08-05 00:21:38 -07:00
topjohnwu
010e4de4e1 Introduce DynamicClassLoader 2019-08-04 23:49:09 -07:00
topjohnwu
41134466ed Upgrade dependencies 2019-08-04 18:33:20 -07:00
topjohnwu
8f07747452 Remove net module 2019-08-04 18:33:20 -07:00
topjohnwu
eb5ce5be1e Fix saving logs
Fix #1722
2019-08-04 14:17:01 -07:00
topjohnwu
71d855e836 Cleanup more code 2019-08-04 13:47:14 -07:00
topjohnwu
33b7ab593c Migrate PatchAPK to Kotlin 2019-08-04 13:00:27 -07:00
topjohnwu
8706d834b4 Update Android Studio 2019-08-02 21:26:00 -07:00
topjohnwu
7cfab33ebb Make sure DownloadService always start with app context 2019-08-02 01:21:22 -07:00
topjohnwu
1ababc8c7f RepoDB does not need to run on main thread 2019-08-02 01:20:16 -07:00
topjohnwu
1f75e63c37 Fix crashes in MarkdownWindow
Fix #1628
2019-08-02 01:16:04 -07:00
topjohnwu
cb3f9b9740 More tweaking to Rx pipeline 2019-08-01 23:08:58 -07:00
topjohnwu
9784353223 Fix ActivityTracker
Koin does not support nullable types
2019-07-29 04:18:05 -07:00
topjohnwu
7d93ca5c73 Modernize MagiskInstaller 2019-07-29 04:05:54 -07:00
topjohnwu
ac20063e86 Disable cache for Magisk Manager 2019-07-29 03:56:35 -07:00
topjohnwu
debaec32af Remove old download progress update system 2019-07-29 00:42:53 -07:00
topjohnwu
0e9b71e7a9 Show notification on error 2019-07-29 00:37:01 -07:00
topjohnwu
85f5ff3c14 Download Magisk Manager via new service 2019-07-29 00:26:18 -07:00
Frieder Bluemle
3d81f167ea Update Gradle wrapper to 5.5.1 2019-07-28 15:32:33 -07:00
Taras
fb70a2e52d Update Ukrainian translation 2019-07-28 15:32:01 -07:00
JoanVC100
460e85a1b5 New lines added and fixs 2019-07-28 15:31:49 -07:00
cheese1
539b64bd57 update german language-file 2019-07-28 15:31:24 -07:00
linar10
90e38a06a2 Update strings.xml 2019-07-28 15:31:03 -07:00
zertyuiop
09ab910630 Added missing strings 2019-07-28 15:30:54 -07:00
topjohnwu
c15f80b33f Improve Rx pipeline 2019-07-28 14:49:06 -07:00
topjohnwu
b2e6ba3c4a Move no thanks from dialogs 2019-07-28 03:54:46 -07:00
topjohnwu
b16f696b0e Cleanups 2019-07-28 03:47:07 -07:00
topjohnwu
9adfb382e8 Only launch FlashActivity if app is foreground 2019-07-28 03:38:27 -07:00
topjohnwu
44368383f4 Fix fetching repo ordering 2019-07-28 02:21:55 -07:00
topjohnwu
d1ff7e0ffe Move extensions to its own package 2019-07-28 02:10:22 -07:00
topjohnwu
42e7db8d13 Modernize Repo class for Magisk modules
- Use Kotlin
- Use Room database
- Use retrofit for networking
- Use RxJava pipeline for repo updating
2019-07-28 01:54:34 -07:00
topjohnwu
0c17ea5755 Migrate Magisk Modules to Kotlin 2019-07-27 15:46:44 -07:00
topjohnwu
cdaff5b39c Update module download pipeline 2019-07-26 02:26:02 -07:00
topjohnwu
2b1b970e78 Update dependencies 2019-07-26 02:00:42 -07:00
topjohnwu
0aebc0a8e3 Use new service to download uninstall.zip 2019-07-25 03:10:24 -07:00
topjohnwu
c3a89f589e Download to proper filename 2019-07-25 01:54:42 -07:00
topjohnwu
971cd73fb3 Dismiss notification on error 2019-07-25 01:37:47 -07:00
topjohnwu
1947860d61 Dismiss notification after flashing 2019-07-25 01:05:06 -07:00
topjohnwu
55aaa421e8 Directly download to target location 2019-07-23 01:31:59 -07:00
topjohnwu
a8932706d8 Consolidate Magisk download subject 2019-07-23 00:55:12 -07:00
topjohnwu
a97972aac0 Update notification once per second 2019-07-23 00:33:28 -07:00
topjohnwu
094c3d559a Minor fixes and cleanups 2019-07-22 01:49:21 -07:00
topjohnwu
6fb032b3c2 Clean ups 2019-07-20 22:37:34 -07:00
topjohnwu
8ca188f4d4 Stream and process module zips 2019-07-20 21:04:06 -07:00
topjohnwu
746a1d8d59 Directly download to magisk.zip for flashing 2019-07-20 14:57:03 -07:00
topjohnwu
63c5e00d86 Update Android Studio 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
9d2e5d6665 Updated design for custom channel field so it matches the other dialog 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
f6045bf8b5 Added custom dialog for download location only 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
e83f40d5c5 Added actions for opening files in the file browser
No icons are added at this time, so crashes might occur
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
e5118418b2 Added option to have custom download location
The location is automatically added to list of supported paths for caching
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
7cd814d917 Updated service to use extra transformer so the service itself is not plagued by unnecessary code 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
78282c1a49 Removed unused entry 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
fd4214ccf3 Fixed minor bugs regarding notification cancellation 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
0785945635 Added appending installers to modules 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
967bdeae7b Updated service architecture and extracted useful tools to separate class 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
452db51669 Updated flash location so it's one layer deeper preventing accidental cache deletion 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
5875ced367 Fixed launching activities on newer systems 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
fbac6bcfd0 Fixed substrate handling multiple downloads at once 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
0dcd3ece9d Updated downloading modules 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
224fff89e3 Updated object usage for module subjects 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
22e73644f9 Added option to run service in foreground right away 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
6a0f6ab319 Updated magisk installer so it uses predownloaded file 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
88a394836f Replaced all install methods with the download service 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
f822c1c2e4 Added default to flash configuration 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
1d16d980b3 Added second slot flashing capability 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
501b18f986 Added default value to magisk subject 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
21ed759e53 Removed duplicate helper 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
8d50dfd93c Fixed overwriting file in download mode
Added prevention of copying itself to itself
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
51e40dd98c Fixed crashes caused by file exposure beyond app bounds 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
b2048379af Fixed uris so in case there's no additional the data one (with zips) is selected instead 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
011539f6f1 Added permission requirements for using service 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
5457c3803f Added remaining methods of installation/flashing/uninstall to service
Updated parameters of patching step and fixed new ordered flashing format
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
b3d777bb6c Updated configuration to hold data when necessary 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
12e00c3054 Updated method naming scheme
Added new configurations
Added flashing methods and annotated viewModel's uri as deprecated in function
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
40b683111c Added an option to disable the new caching mechanism completely 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
9542ca773f Added new CompoundDownloadService which will encapsulate all downloads and should manage post-download events as well
As of now it's still in a development stage and isn't connected to anything
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
8af832a496 Added several calls to FlashActivity so it manages its launch parameters by itself
Its reach will be deepened further in the future commits
2019-07-20 14:57:03 -07:00
Viktor De Pasquale
6836130fda Added overloaded method call for progress notification so it accepts foreign context 2019-07-20 14:57:03 -07:00
Viktor De Pasquale
724893879f Added option to intercept progress while copying files 2019-07-20 14:57:03 -07:00
topjohnwu
736729f5ef Maintain a list of pre-init mounts
Keep track of everything to unmount
2019-07-16 23:54:52 -07:00
topjohnwu
aa47966347 Fix raw_data move constructor 2019-07-16 23:30:54 -07:00
Gozzwip
d64d12afe8 Some fixes 2019-07-16 01:20:10 -07:00
topjohnwu
1f8df419c4 Extract x86 busybox first
Fix #1600
2019-07-16 01:16:29 -07:00
topjohnwu
7ba8202af5 Introduce new root overlay system 2019-07-16 01:08:28 -07:00
topjohnwu
d7b691cf59 Move libutil internal headers out of include path 2019-07-14 23:55:52 -07:00
osm0sis
7058d5e4cd magiskpolicy: rules: fix writing to loop devices using upstream sepolicy 2019-07-14 22:09:26 -07:00
topjohnwu
52fd508fea Do not use std::random_device
Directly read from urandom instead of using std::random_device.
libc++ will use iostream under-the-hood, which brings significant
binary size increase that is not welcomed, especially in magiskinit.
2019-07-14 21:56:21 -07:00
topjohnwu
41045b62dc Introduce more randomness
- Use C++ random generator instead of old and broken rand()
- Randomize string length to piss off stupid detectors
2019-07-14 17:42:49 -07:00
Viktor De Pasquale
188ea2644a Updated downloading magisk to pull the zip from cache if eligible 2019-07-08 11:40:02 -07:00
topjohnwu
4c8f357978 Update to support updated FrankeNDK 2019-07-07 17:38:57 -07:00
Marius
4bb2fd6ba6 Fix typos in german translation 2019-07-07 12:40:20 -07:00
osm0sis
33c9f74508 magiskpolicy: rules: fix rootfs operations with SAR Magisk
- while many newer devices cannot allow / (system partition) to be mounted rw due to compressed fs (e.g. erofs) or logical partitions, it should remain possible to alter rootfs files/directories on those that previously allowed it
2019-07-07 12:33:20 -07:00
osm0sis
f53fe67372 BootSigner: support setting name with no cert/key pair supplied 2019-07-07 12:33:02 -07:00
topjohnwu
51ff724691 Unblock all signals in root shell process
Fix #1563
2019-07-07 12:30:57 -07:00
topjohnwu
291bf93f9d Proper timing 2019-07-07 12:20:47 -07:00
topjohnwu
5fcd629f16 Rearrange su daemon routine 2019-07-07 12:20:19 -07:00
topjohnwu
ab90901793 Use C++ smart pointer for caching su_info 2019-07-07 00:31:49 -07:00
topjohnwu
4f206fd918 Fix compile errors 2019-07-06 23:04:24 -07:00
topjohnwu
7233285437 Use relative symbolic links 2019-07-04 17:58:46 -07:00
John Wu
8e348a11c2 Support non standard image headers
Some Samsung device uses the header version field as extra section size
2019-07-04 11:09:45 -07:00
osm0sis
085ea6d0a1 SignBoot: use verity keys not testkey to correctly follow AOSP 2019-07-04 11:09:45 -07:00
osm0sis
aaf88b1895 BootSigner: add ability to change target name
- supports signing /recovery images
- add as final argument and default to /boot if not supplied so installer scripts remain the same
2019-07-04 11:09:45 -07:00
osm0sis
4f4a9412a3 SignBoot: updates from AOSP for boot_img_hdr_v1 and v2
"Allow recovery-dtbo in recovery.img to be signed" by Hridya Valsaraju:
9bb9f8f857

"boot_signer should support boot header version 2" by Hridya Valsaraju
590e58454d
2019-07-04 11:09:45 -07:00
topjohnwu
a92e039363 Split util headers 2019-07-01 22:58:19 -07:00
topjohnwu
33aa4ca4b7 Move libmincrypt into separate repo 2019-06-30 19:53:03 -07:00
topjohnwu
05658cafc7 Fix typo causing sbin clone failure 2019-06-30 19:24:14 -07:00
topjohnwu
ff3710de66 Minor code changes across all sources 2019-06-30 19:09:31 -07:00
topjohnwu
db8dd9f186 Init code rearrangement 2019-06-30 11:39:13 -07:00
topjohnwu
e8b73ba6d1 Add separate product partition support 2019-06-29 14:19:10 -07:00
topjohnwu
f1112fdf37 Logical Resizable Android Partitions support
The way how logical partition, or "Logical Resizable Android Partitions"
as they say in AOSP source code, is setup makes it impossible to early
mount the partitions from the shared super partition with just
a few lines of code; in fact, AOSP has a whole "fs_mgr" folder which
consist of multiple complex libraries, with 15K lines of code just
to deal with the device mapper shenanigans.

In order to keep the already overly complicated MagiskInit more
managable, I chose NOT to go the route of including fs_mgr directly
into MagiskInit. Luckily, starting from Android Q, Google decided to
split init startup into 3 stages, with the first stage doing _only_
early mount. This is great news, because we can simply let the stock
init do its own thing for us, and we intercept the bootup sequence.

So the workflow can be visualized roughly below:

Magisk First Stage --> First Stage Mount --> Magisk Second Stage --+
   (MagiskInit)         (Original Init)         (MagiskInit)       +
                                                                   +
                                                                   +
     ...Rest of the boot... <-- Second Stage <-- Selinux Setup  <--+
      (__________________ Original Init ____________________)

The catch here is that after doing all the first stage mounting, /init
will pivot /system as root directory (/), leaving us impossible to
regain control after we hand it over. So the solution here is to patch
fstab in /first_stage_ramdisk on-the-fly to redirect /system to
/system_root, making the original init do all the hard work for
us and mount required early mount partitions, but skips the step of
switching root directory. It will also conveniently hand over execution
back to MagiskInit, which we will reuse the routine for patching
root directory in normal system-as-root situations.
2019-06-29 01:25:54 -07:00
osm0sis
a48c4f9e05 magiskboot: don't clobber /overlay with cpio restore anymore
- Magisk "dirty" flashes would remove the /overlay directory which might have been put there by a custom kernel or other mod
- this is a leftover from when Magisk itself used /overlay for placing init.magisk.rc, so just remove this file specifically and leave the rest intact
2019-06-27 18:59:54 -04:00
Rom
19a521d2e9 French translation update 2019-06-27 18:59:29 -04:00
cristisilaghi
dd6e55ac31 [ro] Add translators 2019-06-27 04:10:51 -04:00
osm0sis
b1e63f0f14 Manager: fix ModulesFragment reboot menu
- correct 'booloader' typo breaking bootloader entry
- remove extra bootloader entry Shell.su line which is unnecessary since it's covered by reboot()
- revert to using `reboot recovery` for recovery entry since `svc power reboot recovery` triggers a very disconcerting "Factory data reset" reboot dialog on many devices
- add Reboot to EDL mode option for good measure
2019-06-27 04:09:41 -04:00
topjohnwu
b0e49a4cc8 Kill blastula pool when magiskhide init 2019-06-27 00:49:27 -07:00
topjohnwu
1e94517a72 MagiskHide is coming back strong 2019-06-27 00:28:34 -07:00
topjohnwu
98f60216ac Temporary disable MagiskHide by default
Latest Android Q beta does not like when zygote is ptraced on
boot. Disable it for now until further investigation.
2019-06-25 23:32:07 -07:00
topjohnwu
e29b712108 Start Magisk in SAR 2019-06-25 23:31:59 -07:00
topjohnwu
a462435f2f Load custom sepolicy 2019-06-25 21:34:02 -07:00
topjohnwu
911b8273fe Fix typo in sbin clone 2019-06-25 03:35:25 -07:00
topjohnwu
09935e591a Proper SARInit test 2019-06-25 03:34:54 -07:00
topjohnwu
4a212dba35 Early mount partitions in SAR 2019-06-25 02:47:16 -07:00
topjohnwu
aac9e85e04 More Q cleanup 2019-06-25 02:38:34 -07:00
topjohnwu
bb67a837d3 Adjust class structures 2019-06-24 01:50:47 -07:00
topjohnwu
6cde695194 Remove Q dirty hacks in SARCompat 2019-06-24 01:31:42 -07:00
topjohnwu
a1a1ac0bbb Add sbin overlay to system-as-root 2019-06-24 01:21:33 -07:00
topjohnwu
9ec8bc2166 Boot MagiskInit as actual system-as-root
WIP, no customization. DO NOT USE YET!
2019-06-23 15:14:47 -07:00
topjohnwu
28cd6a75e7 Add missing functions in bionic 2019-06-23 14:54:48 -07:00
topjohnwu
4cc7aced15 Add new util function 2019-06-23 03:53:41 -07:00
topjohnwu
1058aeb04f Label current SAR impl as compat
The current system-as-root magiskinit implementation (converting
root directory in system partition to legacy rootfs setup) is now
considered as backwards compatible only.

The new implementation that is hide and Android Q friendly is coming soon.
2019-06-22 03:18:45 -07:00
topjohnwu
cfec0db947 Delay mounting sbin overlay 2019-06-22 03:14:33 -07:00
topjohnwu
120bd6cd68 v7.3.2 Changelog 2019-06-21 01:12:50 -07:00
JoanVC100
9aef06d1b8 Fix traductors 2019-06-21 04:00:54 -04:00
topjohnwu
e6e9dd751c Upgrade Kotlin 2019-06-21 00:53:40 -07:00
Viktor De Pasquale
5dd677756f Fixed multiple fetch tasks running at once
Disposing wouldn't help since the shell doesn't appear to handle concurrency well
2019-06-21 00:36:37 -07:00
Viktor De Pasquale
b77c590910 Fixed the searchView being collapsed after searching through it
Now they have their state synced with viewModel to allow continuity
2019-06-21 00:36:37 -07:00
Viktor De Pasquale
7e5f2822ae Fix superuser fragment crashes
Fix superuser screen encountering inconsistencies when refreshing the data rapidly
2019-06-21 00:36:01 -07:00
topjohnwu
12bbc7fd6b Update v7.3.1 changelog 2019-06-17 22:15:38 -07:00
topjohnwu
bf9ac8252b Cleanup UpdateInfo 2019-06-16 16:47:30 -07:00
topjohnwu
4a3f5dc619 Cleanups 2019-06-16 14:35:51 -07:00
Viktor De Pasquale
ca156befbd Fixed mapping generic pairs to policy crashing when no policy is found
The policy (app) is now deleted when found invalid (uninstalled)
2019-06-16 16:50:08 -04:00
Viktor De Pasquale
4db41e2ac4 Added attempted fix for parsing data off default thread 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
982a43fce1 Moved diff computation of policy list to the background thread 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
dd76a74e1c Fixed fast scroll button crashing while scrolling to undefined position 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
70cb52b2c7 Fixed fast scroll button being visible when log is empty 2019-06-16 16:50:08 -04:00
topjohnwu
5c7f69acaa Separate SAR and legacy implementation 2019-06-16 12:45:32 -07:00
topjohnwu
f1d9015e5f Move load kernel info out of class 2019-06-15 22:25:09 -07:00
topjohnwu
e8d900c58e Fix typo 2019-06-15 18:12:12 -07:00
topjohnwu
a6241ae912 Fix magiskboot unpack option parsing 2019-06-15 16:15:12 -07:00
topjohnwu
4a697ca2ec Add 7.3.0 changelog 2019-06-14 22:39:31 -07:00
topjohnwu
58bec7f2c9 Update dependencies 2019-06-14 22:39:31 -07:00
Wenlin Shen
213f84985c Update Chinese (Traditional) translate
Update wordings to improve readability.
2019-06-13 21:33:24 -07:00
Viktor De Pasquale
074b1f8c61 Added one-click scroll to the bottom 2019-06-12 16:08:02 +02:00
topjohnwu
326eee8c83 Migrate a lot of classes to Kotlin 2019-06-12 03:29:38 -07:00
topjohnwu
00bff4912e Use svc for reboot if feasible
Close #1488
2019-06-12 00:55:21 -07:00
Viktor De Pasquale
0ce1720516 Fixed magisk log screen lines having multiple lines 2019-06-11 21:52:03 -07:00
osm0sis
ee407472cf magiskboot: allow forcing no recompression on ramdisk.cpio
- when input image had a compressed ramdisk magiskboot had no way to force the repack with the uncompressed ramdisk.cpio since it does not formally recognize cpio as its own format, so add a switch to support forcing repacking to any possible ramdisk format regardless of input image
2019-06-10 21:57:39 -07:00
osm0sis
f341f3b2dd magiskboot: accept forcing recognized but unsupported format compression
- when input image had a different supported format (e.g. gzip) magiskboot would not accept a manually compressed ramdisk or kernel in an unsupported format (e.g. lzop) despite being able to recognize it, so instead would double compress using whatever the input format was, breaking the image with, in effect, a ramdisk.cpio.lzo.gz
2019-06-10 21:56:51 -07:00
Ian Macdonald
8513946e09 'magiskboot hexpatch' will exit with status 1 when nothing patched. 2019-06-10 21:41:40 -07:00
nonnymoose
8ebd9c8927 Use original file type when creating device nodes 2019-06-10 21:41:17 -07:00
topjohnwu
1d54c5144e Fix background update checks 2019-06-10 21:25:42 -07:00
topjohnwu
e40d4318fa Let Kotlin target Java 8 2019-06-10 21:22:07 -07:00
topjohnwu
7756e10779 Rewrite configs with Kotlin delagate properties 2019-06-10 04:37:56 -07:00
topjohnwu
3e58d502d0 Update SettingsFragment to Kotlin 2019-06-09 03:04:37 -07:00
topjohnwu
1c8846dc57 Make PreferenceModel an interface 2019-06-08 16:30:12 -07:00
topjohnwu
2f320c7239 Update ClassMap 2019-06-08 15:34:15 -07:00
topjohnwu
e799918ab6 Update update check service 2019-06-08 15:28:59 -07:00
topjohnwu
86c4928e0f Fix locale settings
AppCompatActivity changed its impl again...
2019-06-08 02:11:10 -07:00
topjohnwu
0293eb5c51 Never refetch magisk version dynamically 2019-06-08 01:44:02 -07:00
topjohnwu
1ee75b6aa6 Download snet package without legacy impl 2019-06-08 01:39:22 -07:00
topjohnwu
4b30b224b5 Remove separate constant class 2019-06-08 00:41:03 -07:00
topjohnwu
16b232d2a3 Enable okhttp logging in debug only 2019-06-07 02:03:17 -07:00
topjohnwu
3f3b1f5b1d Sort policy with app name 2019-06-07 01:24:54 -07:00
topjohnwu
cec017b7bf More MagidkDB fixes 2019-06-07 01:05:54 -07:00
topjohnwu
3123cc1059 Update AndroidX dependencies 2019-06-07 00:27:07 -07:00
topjohnwu
caa9df86bc Switch to R8 friendly room-runtime 2019-06-07 00:17:00 -07:00
topjohnwu
f417389a7a Fix magisk database code in app 2019-06-06 00:39:24 -07:00
topjohnwu
662a5c8ea6 Upgrade Retrofit 2.6.0 2019-06-05 23:41:51 -07:00
topjohnwu
7edfbfb764 Upgrade SDK 2019-06-05 21:33:09 -07:00
415 changed files with 10745 additions and 12855 deletions

2
.gitattributes vendored
View File

@@ -11,7 +11,7 @@
*.bat text eol=crlf *.bat text eol=crlf
# Denote all files that are truly binary and should not be modified. # Denote all files that are truly binary and should not be modified.
chromeos/** binary tools/** binary
*.jar binary *.jar binary
*.exe binary *.exe binary
*.apk binary *.apk binary

6
.gitmodules vendored
View File

@@ -19,3 +19,9 @@
[submodule "nanopb"] [submodule "nanopb"]
path = native/jni/external/nanopb path = native/jni/external/nanopb
url = https://github.com/nanopb/nanopb.git url = https://github.com/nanopb/nanopb.git
[submodule "mincrypt"]
path = native/jni/external/mincrypt
url = https://github.com/topjohnwu/mincrypt.git
[submodule "termux-elf-cleaner"]
path = tools/termux-elf-cleaner
url = https://github.com/termux/termux-elf-cleaner.git

View File

@@ -10,7 +10,7 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
## Bug Reports ## Bug Reports
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that is already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by opening an issue on GitHub or directly in the thread. **Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that are already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by [opening an issue on GitHub](https://github.com/topjohnwu/Magisk/issues) or directly in the thread.
## Building Environment Requirements ## Building Environment Requirements

View File

@@ -17,8 +17,8 @@ android {
applicationId 'com.topjohnwu.magisk' applicationId 'com.topjohnwu.magisk'
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
multiDexEnabled true multiDexEnabled true
versionName configProps['appVersion'] versionName props['appVersion']
versionCode configProps['appVersionCode'] as Integer versionCode props['appVersionCode'] as Integer
} }
buildTypes { buildTypes {
@@ -39,10 +39,14 @@ android {
exclude '/META-INF/*.kotlin_module' exclude '/META-INF/*.kotlin_module'
exclude '/META-INF/rxkotlin.properties' exclude '/META-INF/rxkotlin.properties'
exclude '/androidsupportmultidexversion.txt' exclude '/androidsupportmultidexversion.txt'
exclude '/org/**' exclude '/org/bouncycastle/**'
exclude '/kotlin/**' exclude '/kotlin/**'
exclude '/kotlinx/**' exclude '/kotlinx/**'
} }
kotlinOptions {
jvmTarget = '1.8'
}
} }
androidExtensions { androidExtensions {
@@ -51,21 +55,33 @@ androidExtensions {
dependencies { dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs') implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':net')
implementation project(':shared') implementation project(':shared')
implementation project(':signing') implementation project(':signing')
implementation 'com.github.topjohnwu:jtar:1.0.0' implementation 'com.github.topjohnwu:jtar:1.0.0'
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
implementation 'com.github.skoumalcz:teanity:0.3.3'
implementation 'com.ncapdevi:frag-nav:3.2.0' implementation 'com.ncapdevi:frag-nav:3.2.0'
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
def vMarkwon = '3.0.1' implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
implementation "ru.noties.markwon:core:${vMarkwon}" implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation "ru.noties.markwon:html:${vMarkwon}" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
def vLibsu = '2.5.0' implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}"
def vBAdapt = '3.1.1'
def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter'
implementation "${bindingAdapter}:${vBAdapt}"
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
def vMarkwon = '4.1.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'
def vLibsu = '2.5.1'
implementation "com.github.topjohnwu.libsu:core:${vLibsu}" implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
implementation "com.github.topjohnwu.libsu:io:${vLibsu}" implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
@@ -74,12 +90,13 @@ dependencies {
implementation "org.koin:koin-android:${vKoin}" implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}" implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = "2.5.0" def vRetrofit = '2.6.2'
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}" implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}" implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}" implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = "3.12.0" def vOkHttp = '3.12.6'
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}" implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}" implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
@@ -90,13 +107,27 @@ dependencies {
implementation "se.ansman.kotshi:api:${vKotshi}" implementation "se.ansman.kotshi:api:${vKotshi}"
kapt "se.ansman.kotshi:compiler:${vKotshi}" kapt "se.ansman.kotshi:compiler:${vKotshi}"
modules {
module('androidx.room:room-runtime') {
replacedBy('com.github.topjohnwu:room-runtime')
}
}
def vRoom = "2.2.0"
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
kapt "androidx.room:room-compiler:${vRoom}"
def vNav = "2.1.0"
implementation "androidx.navigation:navigation-fragment-ktx:$vNav"
implementation "androidx.navigation:navigation-ui-ktx:$vNav"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.browser:browser:1.0.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'androidx.preference:preference:1.0.0' implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05' implementation 'androidx.recyclerview:recyclerview:1.1.0-beta05'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.work:work-runtime:2.0.1' implementation 'androidx.work:work-runtime:2.2.0'
implementation 'androidx.transition:transition:1.2.0-alpha01' implementation 'androidx.transition:transition:1.2.0'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha06' implementation 'androidx.core:core-ktx:1.1.0'
implementation 'com.google.android.material:material:1.1.0-beta01'
} }

View File

@@ -16,13 +16,10 @@
# public *; # public *;
#} #}
# Retrofit classes
-keep,allowobfuscation class com.topjohnwu.magisk.data.network.*
# Snet # Snet
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; } -keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback -keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback { -keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
void onResponse(int); void onResponse(int);
} }
@@ -32,16 +29,13 @@
} }
# DelegateWorker # DelegateWorker
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker -keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
# BootSigner # BootSigner
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; } -keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
# Strip logging # Strip logging
-assumenosideeffects class timber.log.Timber.Tree { *; } -assumenosideeffects class timber.log.Timber.Tree { *; }
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
public *** debug(...);
}
# Excessive obfuscation # Excessive obfuscation
-repackageclasses 'a' -repackageclasses 'a'

View File

@@ -3,14 +3,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.magisk"> package="com.topjohnwu.magisk">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<application <application
android:allowBackup="true"
android:name="a.e" android:name="a.e"
android:allowBackup="true"
android:theme="@style/MagiskTheme" android:theme="@style/MagiskTheme"
android:usesCleartextTraffic="true" android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"> tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
@@ -41,9 +42,9 @@
<activity <activity
android:name="a.m" android:name="a.m"
android:exported="false"
android:directBootAware="true" android:directBootAware="true"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="false"
android:theme="@style/MagiskTheme.SU" /> android:theme="@style/MagiskTheme.SU" />
<!-- Receiver --> <!-- Receiver -->
@@ -65,7 +66,8 @@
<!-- Service --> <!-- Service -->
<service android:name="a.j" /> <service android:name="a.j"
android:exported="false" />
<!-- Hardcode GMS version --> <!-- Hardcode GMS version -->
<meta-data <meta-data

View File

@@ -1,7 +1,7 @@
package a; package a;
import com.topjohnwu.magisk.model.download.DownloadModuleService; import com.topjohnwu.magisk.model.download.DownloadService;
public class j extends DownloadModuleService { public class j extends DownloadService {
/* stub */ /* stub */
} }

View File

@@ -2,14 +2,14 @@ package a;
import android.content.Context; import android.content.Context;
import com.topjohnwu.magisk.model.worker.DelegateWorker;
import java.lang.reflect.ParameterizedType;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.work.Worker; import androidx.work.Worker;
import androidx.work.WorkerParameters; import androidx.work.WorkerParameters;
import com.topjohnwu.magisk.base.DelegateWorker;
import java.lang.reflect.ParameterizedType;
public abstract class w<T extends DelegateWorker> extends Worker { public abstract class w<T extends DelegateWorker> extends Worker {
/* Wrapper class to workaround Proguard -keep class * extends Worker */ /* Wrapper class to workaround Proguard -keep class * extends Worker */
@@ -22,7 +22,7 @@ public abstract class w<T extends DelegateWorker> extends Worker {
try { try {
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()) base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
.getActualTypeArguments()[0]).newInstance(); .getActualTypeArguments()[0]).newInstance();
base.setActualWorker(this); base.attachWorker(this);
} catch (Exception ignored) {} } catch (Exception ignored) {}
} }

View File

@@ -1,32 +1,41 @@
package com.topjohnwu.magisk package com.topjohnwu.magisk
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex import androidx.multidex.MultiDex
import androidx.room.Room
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkDatabase_Impl
import com.topjohnwu.magisk.data.database.RepoDatabase
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
import com.topjohnwu.magisk.di.ActivityTracker
import com.topjohnwu.magisk.di.koinModules import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.utils.LocaleManager import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.RootUtils import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.ThreadPoolExecutor
open class App : Application(), Application.ActivityLifecycleCallbacks { open class App : Application() {
lateinit var protectedContext: Context init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
@Volatile Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
private var foreground: Activity? = null Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootUtils::class.java)
Shell.Config.setTimeout(2)
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
RepoDatabase::class.java -> RepoDatabase_Impl()
else -> null
}
}
}
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
super.attachBaseContext(base) super.attachBaseContext(base)
@@ -39,19 +48,7 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
modules(koinModules) modules(koinModules)
} }
protectedContext = baseContext registerActivityLifecycleCallbacks(get<ActivityTracker>())
self = this
deContext = base
if (Build.VERSION.SDK_INT >= 24) {
protectedContext = base.createDeviceProtectedStorageContext()
deContext = protectedContext
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
}
registerActivityLifecycleCallbacks(this)
Networking.init(base)
LocaleManager.setLocale(this) LocaleManager.setLocale(this)
} }
@@ -59,61 +56,4 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
LocaleManager.setLocale(this) LocaleManager.setLocale(this)
} }
//region ActivityLifecycleCallbacks
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
@Synchronized
override fun onActivityResumed(activity: Activity) {
foreground = activity
}
@Synchronized
override fun onActivityPaused(activity: Activity) {
foreground = null
}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
//endregion
private val Context.defaultPrefsName get() = "${packageName}_preferences"
companion object {
@SuppressLint("StaticFieldLeak")
@Deprecated("Use dependency injection")
@JvmStatic
lateinit var self: App
@SuppressLint("StaticFieldLeak")
@Deprecated("Use dependency injection; replace with protectedContext")
@JvmStatic
lateinit var deContext: Context
@Deprecated("Use Rx or similar")
@JvmField
var THREAD_POOL: ThreadPoolExecutor
init {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
Shell.Config.verboseLogging(BuildConfig.DEBUG)
Shell.Config.addInitializers(RootUtils::class.java)
Shell.Config.setTimeout(2)
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
}
@Deprecated("")
@JvmStatic
fun foreground(): Activity? {
val app: App by inject()
return app.foreground
}
}
} }

View File

@@ -1,6 +1,6 @@
package com.topjohnwu.magisk package com.topjohnwu.magisk
import com.topjohnwu.magisk.model.download.DownloadModuleService import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.receiver.GeneralReceiver import com.topjohnwu.magisk.model.receiver.GeneralReceiver
import com.topjohnwu.magisk.model.update.UpdateCheckService import com.topjohnwu.magisk.model.update.UpdateCheckService
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
@@ -16,12 +16,11 @@ object ClassMap {
FlashActivity::class.java to a.f::class.java, FlashActivity::class.java to a.f::class.java,
UpdateCheckService::class.java to a.g::class.java, UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java, GeneralReceiver::class.java to a.h::class.java,
DownloadModuleService::class.java to a.j::class.java, DownloadService::class.java to a.j::class.java,
SuRequestActivity::class.java to a.m::class.java SuRequestActivity::class.java to a.m::class.java
) )
@JvmStatic operator fun <T : Class<*>>get(c: Class<*>): T {
operator fun get(c: Class<*>): Class<*>? { return map.getOrElse(c) { throw IllegalArgumentException() } as T
return map.getOrElse(c) { null } //as? Class<T>
} }
} }

View File

@@ -1,399 +0,0 @@
package com.topjohnwu.magisk;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Xml;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.File;
import java.io.IOException;
import androidx.annotation.Nullable;
import androidx.collection.ArrayMap;
import static com.topjohnwu.magisk.ConfigLeanback.getPrefs;
import static com.topjohnwu.magisk.utils.XAndroidKt.getPackageName;
public final class Config {
private static final ArrayMap<String, Object> defs = new ArrayMap<>();
public static int magiskVersionCode = -1;
// Current status
public static String magiskVersionString = "";
// Update Info
public static String remoteMagiskVersionString = "";
public static int remoteMagiskVersionCode = -1;
public static String magiskLink = "";
public static String magiskNoteLink = "";
public static String magiskMD5 = "";
public static String remoteManagerVersionString = "";
public static int remoteManagerVersionCode = -1;
public static String managerLink = "";
public static String managerNoteLink = "";
public static String uninstallerLink = "";
// Install flags
public static boolean keepVerity = false;
public static boolean keepEnc = false;
public static boolean recovery = false;
public static int suLogTimeout = 14;
public static class Key {
// su configs
public static final String ROOT_ACCESS = "root_access";
public static final String SU_MULTIUSER_MODE = "multiuser_mode";
public static final String SU_MNT_NS = "mnt_ns";
public static final String SU_MANAGER = "requester";
public static final String SU_REQUEST_TIMEOUT = "su_request_timeout";
public static final String SU_AUTO_RESPONSE = "su_auto_response";
public static final String SU_NOTIFICATION = "su_notification";
public static final String SU_REAUTH = "su_reauth";
public static final String SU_FINGERPRINT = "su_fingerprint";
// prefs
public static final String CHECK_UPDATES = "check_update";
public static final String UPDATE_CHANNEL = "update_channel";
public static final String CUSTOM_CHANNEL = "custom_channel";
public static final String LOCALE = "locale";
public static final String DARK_THEME = "dark_theme";
public static final String ETAG_KEY = "ETag";
public static final String REPO_ORDER = "repo_order";
public static final String SHOW_SYSTEM_APP = "show_system";
// system state
public static final String UPDATE_SERVICE_VER = "update_service_version";
public static final String MAGISKHIDE = "magiskhide";
public static final String COREONLY = "disable";
}
public static class Value {
public static final int DEFAULT_CHANNEL = -1;
public static final int STABLE_CHANNEL = 0;
public static final int BETA_CHANNEL = 1;
public static final int CUSTOM_CHANNEL = 2;
public static final int CANARY_CHANNEL = 3;
public static final int CANARY_DEBUG_CHANNEL = 4;
public static final int ROOT_ACCESS_DISABLED = 0;
public static final int ROOT_ACCESS_APPS_ONLY = 1;
public static final int ROOT_ACCESS_ADB_ONLY = 2;
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
public static final int MULTIUSER_MODE_USER = 2;
public static final int NAMESPACE_MODE_GLOBAL = 0;
public static final int NAMESPACE_MODE_REQUESTER = 1;
public static final int NAMESPACE_MODE_ISOLATE = 2;
public static final int NO_NOTIFICATION = 0;
public static final int NOTIFICATION_TOAST = 1;
public static final int SU_PROMPT = 0;
public static final int SU_AUTO_DENY = 1;
public static final int SU_AUTO_ALLOW = 2;
public static final int[] TIMEOUT_LIST = {0, -1, 10, 20, 30, 60};
public static final int ORDER_NAME = 0;
public static final int ORDER_DATE = 1;
}
private static boolean magiskHide = false;
public static void loadMagiskInfo() {
try {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
magiskHide = Shell.su("magiskhide --status").exec().isSuccess();
} catch (NumberFormatException ignored) {
}
}
public static void export() {
// Flush prefs to disk
getPrefs().edit().apply();
Context context = ConfigLeanback.getProtectedContext();
File xml = new File(context.getFilesDir().getParent() + "/shared_prefs",
getPackageName() + "_preferences.xml");
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
}
private static final int PREF_INT = 0;
private static final int PREF_STR_INT = 1;
private static final int PREF_BOOL = 2;
private static final int PREF_STR = 3;
private static final int DB_INT = 4;
private static final int DB_BOOL = 5;
private static final int DB_STR = 6;
private static int getConfigType(String key) {
switch (key) {
case Key.REPO_ORDER:
return PREF_INT;
case Key.SU_REQUEST_TIMEOUT:
case Key.SU_AUTO_RESPONSE:
case Key.SU_NOTIFICATION:
case Key.UPDATE_CHANNEL:
return PREF_STR_INT;
case Key.DARK_THEME:
case Key.SU_REAUTH:
case Key.CHECK_UPDATES:
case Key.MAGISKHIDE:
case Key.COREONLY:
case Key.SHOW_SYSTEM_APP:
return PREF_BOOL;
case Key.CUSTOM_CHANNEL:
case Key.LOCALE:
case Key.ETAG_KEY:
return PREF_STR;
case Key.ROOT_ACCESS:
case Key.SU_MNT_NS:
case Key.SU_MULTIUSER_MODE:
return DB_INT;
case Key.SU_FINGERPRINT:
return DB_BOOL;
case Key.SU_MANAGER:
return DB_STR;
default:
throw new IllegalArgumentException();
}
}
public static void initialize() {
SharedPreferences pref = getPrefs();
SharedPreferences.Editor editor = pref.edit();
File config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS);
if (config.exists()) {
try {
SuFileInputStream is = new SuFileInputStream(config);
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(is, "UTF-8");
parser.nextTag();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG)
continue;
String key = parser.getAttributeValue(null, "name");
String value = parser.getAttributeValue(null, "value");
switch (parser.getName()) {
case "string":
parser.require(XmlPullParser.START_TAG, null, "string");
editor.putString(key, parser.nextText());
parser.require(XmlPullParser.END_TAG, null, "string");
break;
case "boolean":
parser.require(XmlPullParser.START_TAG, null, "boolean");
editor.putBoolean(key, Boolean.parseBoolean(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "boolean");
break;
case "int":
parser.require(XmlPullParser.START_TAG, null, "int");
editor.putInt(key, Integer.parseInt(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "int");
break;
case "long":
parser.require(XmlPullParser.START_TAG, null, "long");
editor.putLong(key, Long.parseLong(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "long");
break;
case "float":
parser.require(XmlPullParser.START_TAG, null, "int");
editor.putFloat(key, Float.parseFloat(value));
parser.nextTag();
parser.require(XmlPullParser.END_TAG, null, "int");
break;
default:
parser.next();
}
}
} catch (IOException | XmlPullParserException e) {
e.printStackTrace();
}
editor.remove(Key.ETAG_KEY);
editor.apply();
editor = pref.edit();
config.delete();
}
// Set defaults if not set
setDefs(pref, editor);
// These settings are from actual device state
editor.putBoolean(Key.MAGISKHIDE, magiskHide)
.putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
.putInt(Key.UPDATE_SERVICE_VER, Const.UPDATE_SERVICE_VER)
.apply();
}
@SuppressWarnings("unchecked")
public static <T> T get(String key) {
switch (getConfigType(key)) {
case PREF_INT:
return (T) (Integer) getPrefs().getInt(key, getDef(key));
case PREF_STR_INT:
return (T) (Integer) Utils.getPrefsInt(getPrefs(), key, getDef(key));
case PREF_BOOL:
return (T) (Boolean) getPrefs().getBoolean(key, getDef(key));
case PREF_STR:
return (T) getPrefs().getString(key, getDef(key));
case DB_INT:
return (T) (Integer) ConfigLeanback.get(key, (Integer) getDef(key));
case DB_BOOL:
return (T) (Boolean) (ConfigLeanback.get(key, getDef(key) ? 1 : 0) != 0);
case DB_STR:
return (T) ConfigLeanback.get(key, getDef(key));
}
/* Will never get here (IllegalArgumentException in getConfigType) */
return null;
}
public static void set(String key, Object val) {
switch (getConfigType(key)) {
case PREF_INT:
getPrefs().edit().putInt(key, (int) val).apply();
break;
case PREF_STR_INT:
getPrefs().edit().putString(key, String.valueOf(val)).apply();
break;
case PREF_BOOL:
getPrefs().edit().putBoolean(key, (boolean) val).apply();
break;
case PREF_STR:
getPrefs().edit().putString(key, (String) val).apply();
break;
case DB_INT:
ConfigLeanback.put(key, (int) val);
break;
case DB_BOOL:
ConfigLeanback.put(key, (boolean) val ? 1 : 0);
break;
case DB_STR:
ConfigLeanback.put(key, (String) val);
break;
}
}
public static void remove(String key) {
switch (getConfigType(key)) {
case PREF_INT:
case PREF_STR_INT:
case PREF_BOOL:
case PREF_STR:
getPrefs().edit().remove(key).apply();
break;
case DB_BOOL:
case DB_INT:
ConfigLeanback.delete(key);
break;
case DB_STR:
ConfigLeanback.put(key, null);
break;
}
}
static {
/* Set default configurations */
// prefs int
defs.put(Key.REPO_ORDER, Value.ORDER_DATE);
// prefs string int
defs.put(Key.SU_REQUEST_TIMEOUT, 10);
defs.put(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT);
defs.put(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST);
defs.put(Key.UPDATE_CHANNEL, Utils.isCanary() ?
Value.CANARY_DEBUG_CHANNEL : Value.DEFAULT_CHANNEL);
// prefs bool
defs.put(Key.CHECK_UPDATES, true);
defs.put(Key.DARK_THEME, true);
//defs.put(Key.SU_REAUTH, false);
//defs.put(Key.SHOW_SYSTEM_APP, false);
// prefs string
defs.put(Key.CUSTOM_CHANNEL, "");
defs.put(Key.LOCALE, "");
//defs.put(Key.ETAG_KEY, null);
// db int
defs.put(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB);
defs.put(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER);
defs.put(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY);
// db bool
//defs.put(Key.SU_FINGERPRINT, false);
// db strings
//defs.put(Key.SU_MANAGER, null);
}
@SuppressWarnings("unchecked")
@Nullable
private static <T> T getDef(String key) throws IllegalArgumentException {
Object val = defs.get(key);
switch (getConfigType(key)) {
case PREF_INT:
case DB_INT:
case PREF_STR_INT:
return val != null ? (T) val : (T) (Integer) 0;
case DB_BOOL:
case PREF_BOOL:
return val != null ? (T) val : (T) (Boolean) false;
case DB_STR:
case PREF_STR:
return (T) val;
}
/* Will never get here (IllegalArgumentException in getConfigType) */
return null;
}
private static void setDefs(SharedPreferences pref, SharedPreferences.Editor editor) {
for (String key : defs.keySet()) {
Object value = defs.get(key);
int type = getConfigType(key);
switch (type) {
case DB_INT:
editor.putString(key, String.valueOf(ConfigLeanback.get(key, (Integer) value)));
continue;
case DB_STR:
editor.putString(key, ConfigLeanback.get(key, String.valueOf(value)));
continue;
case DB_BOOL:
int bs = ConfigLeanback.get(key, -1);
editor.putBoolean(key, bs < 0 ? (Boolean) value : bs != 0);
continue;
}
if (pref.contains(key))
continue;
switch (type) {
case PREF_INT:
editor.putInt(key, (Integer) value);
break;
case PREF_STR_INT:
editor.putString(key, String.valueOf(value));
break;
case PREF_STR:
editor.putString(key, (String) value);
break;
case PREF_BOOL:
editor.putBoolean(key, (Boolean) value);
break;
}
}
}
}

View File

@@ -0,0 +1,208 @@
package com.topjohnwu.magisk
import android.content.Context
import android.content.SharedPreferences
import android.os.Environment
import android.util.Xml
import androidx.core.content.edit
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.FingerprintHelper
import com.topjohnwu.magisk.utils.Utils
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
object Config : PreferenceModel, DBConfig {
override val stringDao: StringDao by inject()
override val settingsDao: SettingsDao by inject()
override val context: Context by inject(Protected)
object Key {
// db configs
const val ROOT_ACCESS = "root_access"
const val SU_MULTIUSER_MODE = "multiuser_mode"
const val SU_MNT_NS = "mnt_ns"
const val SU_MANAGER = "requester"
const val SU_FINGERPRINT = "su_fingerprint"
// prefs
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
const val SU_AUTO_RESPONSE = "su_auto_response"
const val SU_NOTIFICATION = "su_notification"
const val SU_REAUTH = "su_reauth"
const val CHECK_UPDATES = "check_update"
const val UPDATE_CHANNEL = "update_channel"
const val CUSTOM_CHANNEL = "custom_channel"
const val LOCALE = "locale"
const val DARK_THEME = "dark_theme"
const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system"
const val DOWNLOAD_PATH = "download_path"
// system state
const val MAGISKHIDE = "magiskhide"
const val COREONLY = "disable"
}
object Value {
// Update channels
const val DEFAULT_CHANNEL = -1
const val STABLE_CHANNEL = 0
const val BETA_CHANNEL = 1
const val CUSTOM_CHANNEL = 2
const val CANARY_CHANNEL = 3
const val CANARY_DEBUG_CHANNEL = 4
// root access mode
const val ROOT_ACCESS_DISABLED = 0
const val ROOT_ACCESS_APPS_ONLY = 1
const val ROOT_ACCESS_ADB_ONLY = 2
const val ROOT_ACCESS_APPS_AND_ADB = 3
// su multiuser
const val MULTIUSER_MODE_OWNER_ONLY = 0
const val MULTIUSER_MODE_OWNER_MANAGED = 1
const val MULTIUSER_MODE_USER = 2
// su mnt ns
const val NAMESPACE_MODE_GLOBAL = 0
const val NAMESPACE_MODE_REQUESTER = 1
const val NAMESPACE_MODE_ISOLATE = 2
// su notification
const val NO_NOTIFICATION = 0
const val NOTIFICATION_TOAST = 1
// su auto response
const val SU_PROMPT = 0
const val SU_AUTO_DENY = 1
const val SU_AUTO_ALLOW = 2
// su timeout
val TIMEOUT_LIST = intArrayOf(0, -1, 10, 20, 30, 60)
// repo order
const val ORDER_NAME = 0
const val ORDER_DATE = 1
}
private val defaultChannel =
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
else Value.DEFAULT_CHANNEL
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
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 suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
var darkTheme by preference(Key.DARK_THEME, true)
var suReAuth by preference(Key.SU_REAUTH, false)
var checkUpdate by preference(Key.CHECK_UPDATES, true)
var magiskHide by preference(Key.MAGISKHIDE, true)
var coreOnly by preference(Key.COREONLY, false)
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
var customChannelUrl by preference(Key.CUSTOM_CHANNEL, "")
var locale by preference(Key.LOCALE, "")
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
var suManager by dbStrings(Key.SU_MANAGER, "", true)
// Always return a path in external storage where we can write
val downloadDirectory get() =
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
fun initialize() = prefs.edit {
parsePrefs(this)
if (!prefs.contains(Key.UPDATE_CHANNEL))
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
// Get actual state
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
// Write database configs
putString(Key.ROOT_ACCESS, rootMode.toString())
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
}
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
if (config.exists()) runCatching {
val input = SuFileInputStream(config).buffered()
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, "UTF-8")
parser.nextTag()
parser.require(XmlPullParser.START_TAG, null, "map")
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.eventType != XmlPullParser.START_TAG)
continue
val key: String = parser.getAttributeValue(null, "name")
fun value() = parser.getAttributeValue(null, "value")!!
when (parser.name) {
"string" -> {
parser.require(XmlPullParser.START_TAG, null, "string")
putString(key, parser.nextText())
parser.require(XmlPullParser.END_TAG, null, "string")
}
"boolean" -> {
parser.require(XmlPullParser.START_TAG, null, "boolean")
putBoolean(key, value().toBoolean())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "boolean")
}
"int" -> {
parser.require(XmlPullParser.START_TAG, null, "int")
putInt(key, value().toInt())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "int")
}
"long" -> {
parser.require(XmlPullParser.START_TAG, null, "long")
putLong(key, value().toLong())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "long")
}
"float" -> {
parser.require(XmlPullParser.START_TAG, null, "int")
putFloat(key, value().toFloat())
parser.nextTag()
parser.require(XmlPullParser.END_TAG, null, "int")
}
else -> parser.next()
}
}
config.delete()
}
}
fun export() {
// Flush prefs to disk
prefs.edit().commit()
val xml = File(
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
"${packageName}_preferences.xml"
)
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
}
}

View File

@@ -1,52 +0,0 @@
package com.topjohnwu.magisk
import android.content.Context
import android.content.SharedPreferences
import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.data.repository.SettingRepository
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.utils.inject
object ConfigLeanback {
@JvmStatic
val protectedContext: Context by inject(Protected)
@JvmStatic
val prefs: SharedPreferences by inject()
private val settingRepo: SettingRepository by inject()
private val stringRepo: StringRepository by inject()
@JvmStatic
@AnyThread
fun put(key: String, value: Int) {
settingRepo.put(key, value).subscribeK()
}
@JvmStatic
@WorkerThread
fun get(key: String, defaultValue: Int): Int =
settingRepo.fetch(key, defaultValue).blockingGet()
@JvmStatic
@AnyThread
fun put(key: String, value: String?) {
val task = value?.let { stringRepo.put(key, it) } ?: stringRepo.delete(key)
task.subscribeK()
}
@JvmStatic
@WorkerThread
fun get(key: String, defaultValue: String?): String =
stringRepo.fetch(key, defaultValue.orEmpty()).blockingGet()
@JvmStatic
@AnyThread
fun delete(key: String) {
settingRepo.delete(key).subscribeK()
}
}

View File

@@ -1,44 +1,26 @@
package com.topjohnwu.magisk package com.topjohnwu.magisk
import android.os.Environment
import android.os.Process import android.os.Process
import java.io.File import java.io.File
object Const { object Const {
const val DEBUG_TAG = "MagiskManager"
// APK content
const val ANDROID_MANIFEST = "AndroidManifest.xml"
const val SU_KEYSTORE_KEY = "su_key"
// Paths // Paths
const val MAGISK_PATH = "/sbin/.magisk/img" const val MAGISK_PATH = "/sbin/.magisk/img"
@JvmField var MAGISK_DISABLE_FILE = File("xxx")
val EXTERNAL_PATH: File =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
@JvmField
var MAGISK_DISABLE_FILE: File = File("xxx")
const val TMP_FOLDER_PATH = "/dev/tmp" const val TMP_FOLDER_PATH = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log" const val MAGISK_LOG = "/cache/magisk.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
// Versions // Versions
const val UPDATE_SERVICE_VER = 1 const val SNET_EXT_VER = 13
const val SNET_EXT_VER = 12 const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
@JvmField // Misc
val USER_ID = Process.myUid() / 100000 const val ANDROID_MANIFEST = "AndroidManifest.xml"
// Generic
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log" const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
init { val USER_ID = Process.myUid() / 100000
EXTERNAL_PATH.mkdirs()
}
object MagiskVersion { object MagiskVersion {
const val MIN_SUPPORT = 18000 const val MIN_SUPPORT = 18000
@@ -59,36 +41,28 @@ object Const {
} }
object Url { object Url {
@Deprecated("This shouldn't be used. There's literally no need for it")
const val REPO_URL =
"https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d"
const val FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s"
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip" const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
const val MODULE_INSTALLER =
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu" const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
const val PATREON_URL = "https://www.patreon.com/topjohnwu" const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val TWITTER_URL = "https://twitter.com/topjohnwu" const val TWITTER_URL = "https://twitter.com/topjohnwu"
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382" const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk" const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
@JvmField
val SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk")
@JvmField
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
private fun getRaw(where: String, name: String) = const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
"https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s".format(where, name) const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
} }
object Key { object Key {
// others // others
const val LINK_KEY = "Link" const val LINK_KEY = "Link"
const val IF_NONE_MATCH = "If-None-Match" const val IF_NONE_MATCH = "If-None-Match"
const val ETAG_KEY = "ETag"
// intents // intents
const val OPEN_SECTION = "section" const val OPEN_SECTION = "section"
const val INTENT_SET_NAME = "filename" const val INTENT_SET_APP = "app_json"
const val INTENT_SET_LINK = "link"
const val FLASH_ACTION = "action" const val FLASH_ACTION = "action"
const val FLASH_DATA = "additional_data"
const val DISMISS_ID = "dismiss_id"
const val BROADCAST_MANAGER_UPDATE = "manager_update" const val BROADCAST_MANAGER_UPDATE = "manager_update"
const val BROADCAST_REBOOT = "reboot" const val BROADCAST_REBOOT = "reboot"
} }
@@ -101,5 +75,4 @@ object Const {
const val UNINSTALL = "uninstall" const val UNINSTALL = "uninstall"
} }
} }

View File

@@ -1,20 +0,0 @@
package com.topjohnwu.magisk
import android.os.Process
object Constants {
// Paths
val MAGISK_PATH = "/sbin/.magisk/img"
val MAGISK_LOG = "/cache/magisk.log"
val USER_ID get() = Process.myUid() / 100000
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
const val GITHUB_URL = "https://github.com/"
const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
}

View File

@@ -0,0 +1,26 @@
package com.topjohnwu.magisk
import com.topjohnwu.magisk.model.entity.UpdateInfo
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
object Info {
var magiskVersionCode = -1
var magiskVersionString = ""
var remote = UpdateInfo()
var keepVerity = false
var keepEnc = false
var recovery = false
fun loadMagiskInfo() {
runCatching {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
magiskVersionCode = ShellUtils.fastCmd("magisk -V").toInt()
Config.magiskHide = Shell.su("magiskhide --status").exec().isSuccess
}
}
}

View File

@@ -1,47 +0,0 @@
package com.topjohnwu.magisk
import android.content.Context
import com.topjohnwu.magisk.KConfig.UpdateChannel.STABLE
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.inject
object KConfig : PreferenceModel() {
override val context: Context by inject(Protected)
override val fileName: String = "${context.packageName}_preferences"
private var internalUpdateChannel by preference(Config.Key.UPDATE_CHANNEL, STABLE.id.toString())
var useCustomTabs by preference("useCustomTabs", true)
@JvmStatic
var customUpdateChannel by preference(Config.Key.CUSTOM_CHANNEL, "")
@JvmStatic
var updateChannel: UpdateChannel
get() = UpdateChannel.byId(internalUpdateChannel.toIntOrNull() ?: STABLE.id)
set(value) {
internalUpdateChannel = value.id.toString()
}
internal const val DEFAULT_CHANNEL = "topjohnwu/magisk_files"
enum class UpdateChannel(val id: Int) {
STABLE(Config.Value.STABLE_CHANNEL),
BETA(Config.Value.BETA_CHANNEL),
CANARY(Config.Value.CANARY_CHANNEL),
CANARY_DEBUG(Config.Value.CANARY_DEBUG_CHANNEL),
CUSTOM(Config.Value.CUSTOM_CHANNEL);
companion object {
fun byId(id: Int) = when (id) {
Config.Value.STABLE_CHANNEL -> STABLE
Config.Value.BETA_CHANNEL -> BETA
Config.Value.CUSTOM_CHANNEL -> CUSTOM
Config.Value.CANARY_CHANNEL -> CANARY
Config.Value.CANARY_DEBUG_CHANNEL -> CANARY_DEBUG
else -> STABLE
}
}
}
}

View File

@@ -0,0 +1,123 @@
package com.topjohnwu.magisk.base
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SparseArrayCompat
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.extensions.set
import com.topjohnwu.magisk.model.events.EventHandler
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.currentLocale
import kotlin.random.Random
typealias RequestCallback = BaseActivity<*, *>.(Int, Intent?) -> Unit
abstract class BaseActivity<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
AppCompatActivity(), EventHandler {
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
protected abstract val viewModel: ViewModel
protected open val snackbarView get() = binding.root
protected open val navHostId: Int = 0
protected open val defaultPosition: Int = 0
private val resultCallbacks by lazy { SparseArrayCompat<RequestCallback>() }
init {
val theme = if (Config.darkTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(theme)
}
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
config?.setLocale(currentLocale)
super.applyOverrideConfiguration(config)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleManager.getLocaleContext(base))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.viewEvents.observe(this, viewEventObserver)
binding = DataBindingUtil.setContentView<Binding>(this, layoutRes).apply {
setVariable(BR.viewModel, viewModel)
lifecycleOwner = this@BaseActivity
}
}
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build()
val ungranted = permissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}
if (ungranted.isEmpty()) {
request.onSuccess()
} else {
val requestCode = Random.nextInt(256, 512)
resultCallbacks[requestCode] = { result, _ ->
if (result > 0)
request.onSuccess()
else
request.onFailure()
}
ActivityCompat.requestPermissions(this, ungranted.toTypedArray(), requestCode)
}
}
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
var success = true
for (res in grantResults) {
if (res != PackageManager.PERMISSION_GRANTED) {
success = false
break
}
}
resultCallbacks[requestCode]?.apply {
resultCallbacks.remove(requestCode)
invoke(this@BaseActivity, if (success) 1 else -1, null)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.apply {
resultCallbacks.remove(requestCode)
invoke(this@BaseActivity, resultCode, data)
}
}
fun startActivityForResult(intent: Intent, requestCode: Int, listener: RequestCallback) {
resultCallbacks[requestCode] = listener
startActivityForResult(intent, requestCode)
}
}

View File

@@ -0,0 +1,50 @@
package com.topjohnwu.magisk.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.CallSuper
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.model.events.EventHandler
import com.topjohnwu.magisk.model.events.ViewEvent
abstract class BaseFragment<ViewModel : BaseViewModel, Binding : ViewDataBinding> :
Fragment(), EventHandler {
protected val activity get() = requireActivity() as BaseActivity<*, *>
protected lateinit var binding: Binding
protected abstract val layoutRes: Int
protected abstract val viewModel: ViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.viewEvents.observe(this, viewEventObserver)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate<Binding>(inflater, layoutRes, container, false).apply {
setVariable(BR.viewModel, viewModel)
lifecycleOwner = this@BaseFragment
}
return binding.root
}
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
activity.onEventDispatched(event)
}
open fun onBackPressed(): Boolean = false
}

View File

@@ -0,0 +1,56 @@
package com.topjohnwu.magisk.base
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.preference.*
import androidx.recyclerview.widget.RecyclerView
import org.koin.android.ext.android.inject
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected val prefs: SharedPreferences by inject()
protected val activity get() = requireActivity() as BaseActivity<*, *>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = super.onCreateView(inflater, container, savedInstanceState)
prefs.registerOnSharedPreferenceChangeListener(this)
return v
}
override fun onDestroyView() {
prefs.unregisterOnSharedPreferenceChangeListener(this)
super.onDestroyView()
}
private fun setAllPreferencesToAvoidHavingExtraSpace(preference: Preference) {
preference.isIconSpaceReserved = false
if (preference is PreferenceGroup)
for (i in 0 until preference.preferenceCount)
setAllPreferencesToAvoidHavingExtraSpace(preference.getPreference(i))
}
override fun setPreferenceScreen(preferenceScreen: PreferenceScreen?) {
if (preferenceScreen != null)
setAllPreferencesToAvoidHavingExtraSpace(preferenceScreen)
super.setPreferenceScreen(preferenceScreen)
}
override fun onCreateAdapter(preferenceScreen: PreferenceScreen?): RecyclerView.Adapter<*> =
object : PreferenceGroupAdapter(preferenceScreen) {
@SuppressLint("RestrictedApi")
override fun onPreferenceHierarchyChange(preference: Preference?) {
if (preference != null)
setAllPreferencesToAvoidHavingExtraSpace(preference)
super.onPreferenceHierarchyChange(preference)
}
}
}

View File

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

View File

@@ -1,16 +1,29 @@
package com.topjohnwu.magisk.ui.base package com.topjohnwu.magisk.base.viewmodel
import android.app.Activity import android.app.Activity
import com.skoumal.teanity.extensions.doOnSubscribeUi import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.skoumal.teanity.viewmodel.LoadingViewModel import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.events.BackPressEvent import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
abstract class MagiskViewModel : LoadingViewModel() { abstract class BaseViewModel(
initialState: State = State.LOADING
) : LoadingViewModel(initialState) {
val isConnected = KObservableField(false)
init {
ReactiveNetwork.observeNetworkConnectivity(get())
.subscribeK { isConnected.value = it.available() }
.add()
}
fun withView(action: Activity.() -> Unit) { fun withView(action: Activity.() -> Unit) {
ViewActionEvent(action).publish() ViewActionEvent(action).publish()

View File

@@ -0,0 +1,78 @@
package com.topjohnwu.magisk.base.viewmodel
import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR
import io.reactivex.*
abstract class LoadingViewModel(defaultState: State = State.LOADING) :
StatefulViewModel<LoadingViewModel.State>(defaultState) {
val loading @Bindable get() = state == State.LOADING
val loaded @Bindable get() = state == State.LOADED
val loadingFailed @Bindable get() = state == State.LOADING_FAILED
@Deprecated(
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
ReplaceWith("state = State.LOADING", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
DeprecationLevel.WARNING
)
fun setLoading() {
state = State.LOADING
}
@Deprecated(
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
ReplaceWith("state = State.LOADED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
DeprecationLevel.WARNING
)
fun setLoaded() {
state = State.LOADED
}
@Deprecated(
"Direct access is recommended since 0.2. This access method will be removed in 1.0",
ReplaceWith("state = State.LOADING_FAILED", "com.topjohnwu.magisk.base.viewmodel.LoadingViewModel.State"),
DeprecationLevel.WARNING
)
fun setLoadingFailed() {
state = State.LOADING_FAILED
}
override fun notifyStateChanged() {
notifyPropertyChanged(BR.loading)
notifyPropertyChanged(BR.loaded)
notifyPropertyChanged(BR.loadingFailed)
}
enum class State {
LOADED, LOADING, LOADING_FAILED
}
//region Rx
protected fun <T> Observable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
protected fun <T> Single<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
protected fun <T> Maybe<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
.doOnSuccess { if (allowFinishing) viewModel.state = State.LOADED }
protected fun <T> Flowable<T>.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnNext { if (allowFinishing) viewModel.state = State.LOADED }
protected fun Completable.applyViewModel(viewModel: LoadingViewModel, allowFinishing: Boolean = true) =
doOnSubscribe { viewModel.state = State.LOADING }
.doOnError { viewModel.state = State.LOADING_FAILED }
.doOnComplete { if (allowFinishing) viewModel.state = State.LOADED }
//endregion
}

View File

@@ -0,0 +1,46 @@
package com.topjohnwu.magisk.base.viewmodel
import androidx.databinding.Observable
import androidx.databinding.PropertyChangeRegistry
import androidx.lifecycle.ViewModel
/**
* Copy of [android.databinding.BaseObservable] which extends [ViewModel]
*/
abstract class ObservableViewModel : TeanityViewModel(), Observable {
@Transient
private var callbacks: PropertyChangeRegistry? = null
@Synchronized
override fun addOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
if (callbacks == null) {
callbacks = PropertyChangeRegistry()
}
callbacks?.add(callback)
}
@Synchronized
override fun removeOnPropertyChangedCallback(callback: Observable.OnPropertyChangedCallback) {
callbacks?.remove(callback)
}
/**
* Notifies listeners that all properties of this instance have changed.
*/
@Synchronized
fun notifyChange() {
callbacks?.notifyCallbacks(this, 0, null)
}
/**
* Notifies listeners that a specific property has changed. The getter for the property
* that changes should be marked with [android.databinding.Bindable] to generate a field in
* `BR` to be used as `fieldId`.
*
* @param fieldId The generated BR id for the Bindable field.
*/
fun notifyPropertyChanged(fieldId: Int) {
callbacks?.notifyCallbacks(this, fieldId, null)
}
}

View File

@@ -0,0 +1,15 @@
package com.topjohnwu.magisk.base.viewmodel
abstract class StatefulViewModel<State : Enum<*>>(
val defaultState: State
) : ObservableViewModel() {
var state: State = defaultState
set(value) {
field = value
notifyStateChanged()
}
open fun notifyStateChanged() = Unit
}

View File

@@ -0,0 +1,33 @@
package com.topjohnwu.magisk.base.viewmodel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.topjohnwu.magisk.model.events.SimpleViewEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
abstract class TeanityViewModel : ViewModel() {
private val disposables = CompositeDisposable()
private val _viewEvents = MutableLiveData<ViewEvent>()
val viewEvents: LiveData<ViewEvent> get() = _viewEvents
override fun onCleared() {
super.onCleared()
disposables.clear()
}
fun <Event : ViewEvent> Event.publish() {
_viewEvents.value = this
}
fun Int.publish() {
_viewEvents.value = SimpleViewEvent(this)
}
fun Disposable.add() {
disposables.add(this)
}
}

View File

@@ -1,6 +1,5 @@
package com.topjohnwu.magisk.data.database package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.data.database.base.* import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.toLog import com.topjohnwu.magisk.model.entity.toLog
@@ -12,7 +11,7 @@ class LogDao : BaseDao() {
override val table = DatabaseDefinition.Table.LOG override val table = DatabaseDefinition.Table.LOG
fun deleteOutdated( fun deleteOutdated(
suTimeout: Long = Config.suLogTimeout * TimeUnit.DAYS.toMillis(1) suTimeout: Long = TimeUnit.DAYS.toMillis(14)
) = query<Delete> { ) = query<Delete> {
condition { condition {
lessThan("time", suTimeout.toString()) lessThan("time", suTimeout.toString())

View File

@@ -2,12 +2,13 @@ package com.topjohnwu.magisk.data.database
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.Constants import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.base.* import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.extensions.now
import com.topjohnwu.magisk.model.entity.MagiskPolicy import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toMap import com.topjohnwu.magisk.model.entity.toMap
import com.topjohnwu.magisk.model.entity.toPolicy import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.utils.now import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -47,12 +48,7 @@ class PolicyDao(
condition { condition {
equals("uid", uid) equals("uid", uid)
} }
}.map { it.firstOrNull()?.toPolicy(context.packageManager) } }.map { it.first().toPolicySafe() }
.doOnError {
if (it is PackageManager.NameNotFoundException) {
delete(uid).subscribe()
}
}
fun update(policy: MagiskPolicy) = query<Replace> { fun update(policy: MagiskPolicy) = query<Replace> {
values(policy.toMap()) values(policy.toMap())
@@ -60,10 +56,20 @@ class PolicyDao(
fun fetchAll() = query<Select> { fun fetchAll() = query<Select> {
condition { condition {
equals("uid/100000", Constants.USER_ID) equals("uid/100000", Const.USER_ID)
} }
}.flattenAsFlowable { it } }.map { it.mapNotNull { it.toPolicySafe() } }
.map { it.toPolicy(context.packageManager) }
.toList()
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
return runCatching { toPolicy(context.packageManager) }.getOrElse {
Timber.e(it)
if (it is PackageManager.NameNotFoundException) {
val uid = getOrElse("uid") { null } ?: return null
delete(uid).subscribe()
}
null
}
}
} }

View File

@@ -0,0 +1,72 @@
package com.topjohnwu.magisk.data.database
import androidx.room.*
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.entity.module.Repo
@Dao
abstract class RepoDao {
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(etag) = addEtagRaw(RepoEtag(0, etag))
get() = etagRaw()?.key.orEmpty()
fun clear() {
clearRepos()
clearEtag()
}
@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>
@Delete
abstract fun removeRepo(repo: Repo)
@Query("DELETE FROM repos WHERE id = :id")
abstract fun removeRepo(id: String)
@Query("DELETE FROM repos WHERE id IN (:idList)")
abstract fun removeRepos(idList: Collection<String>)
@Query("SELECT * FROM etag")
protected abstract fun etagRaw(): RepoEtag?
@Insert(onConflict = OnConflictStrategy.REPLACE)
protected abstract fun addEtagRaw(etag: RepoEtag)
@Query("DELETE FROM repos")
protected abstract fun clearRepos()
@Query("DELETE FROM etag")
protected abstract fun clearEtag()
}
data class RepoID(
@PrimaryKey val id: String
)
@Entity(tableName = "etag")
data class RepoEtag(
@PrimaryKey val id: Int,
val key: String
)

View File

@@ -0,0 +1,11 @@
package com.topjohnwu.magisk.data.database
import androidx.room.Database
import androidx.room.RoomDatabase
import com.topjohnwu.magisk.model.entity.module.Repo
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
abstract class RepoDatabase : RoomDatabase() {
abstract fun repoDao() : RepoDao
}

View File

@@ -1,118 +0,0 @@
package com.topjohnwu.magisk.data.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.model.entity.Repo;
import java.util.HashSet;
import java.util.Set;
@Deprecated
public class RepoDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VER = 5;
private static final String TABLE_NAME = "repos";
private final SQLiteDatabase mDb;
@Deprecated
public RepoDatabaseHelper(Context context) {
super(context, "repo.db", null, DATABASE_VER);
mDb = getWritableDatabase();
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, DATABASE_VER);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion != newVersion) {
// Nuke old DB and create new table
db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME);
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))");
Config.remove(Config.Key.ETAG_KEY);
}
}
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, 0, DATABASE_VER);
}
@Deprecated
public void clearRepo() {
mDb.delete(TABLE_NAME, null, null);
}
@Deprecated
public void removeRepo(String id) {
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
}
@Deprecated
public void removeRepo(Repo repo) {
removeRepo(repo.getId());
}
@Deprecated
public void removeRepo(Iterable<String> list) {
for (String id : list) {
if (id == null) continue;
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
}
}
@Deprecated
public void addRepo(Repo repo) {
mDb.replace(TABLE_NAME, null, repo.getContentValues());
}
@Deprecated
public Repo getRepo(String id) {
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[]{id}, null, null, null)) {
if (c.moveToNext()) {
return new Repo(c);
}
}
return null;
}
@Deprecated
public Cursor getRawCursor() {
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
}
@Deprecated
public Cursor getRepoCursor() {
String orderBy = null;
switch ((int) Config.get(Config.Key.REPO_ORDER)) {
case Config.Value.ORDER_NAME:
orderBy = "name COLLATE NOCASE";
break;
case Config.Value.ORDER_DATE:
orderBy = "last_update DESC";
}
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy);
}
@Deprecated
public Set<String> getRepoIDSet() {
HashSet<String> set = new HashSet<>(300);
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex("id")));
}
}
return set;
}
}

View File

@@ -10,11 +10,12 @@ class SettingsDao : BaseDao() {
condition { equals("key", key) } condition { equals("key", key) }
}.ignoreElement() }.ignoreElement()
fun put(key: String, value: Int) = query<Insert> { fun put(key: String, value: Int) = query<Replace> {
values(key to value.toString()) values("key" to key, "value" to value)
}.ignoreElement() }.ignoreElement()
fun fetch(key: String, default: Int = -1) = query<Select> { fun fetch(key: String, default: Int = -1) = query<Select> {
fields("value")
condition { equals("key", key) } condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default } }.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }

View File

@@ -10,8 +10,8 @@ class StringDao : BaseDao() {
condition { equals("key", key) } condition { equals("key", key) }
}.ignoreElement() }.ignoreElement()
fun put(key: String, value: String) = query<Insert> { fun put(key: String, value: String) = query<Replace> {
values(key to value) values("key" to key, "value" to value)
}.ignoreElement() }.ignoreElement()
fun fetch(key: String, default: String = "") = query<Select> { fun fetch(key: String, default: String = "") = query<Select> {

View File

@@ -32,11 +32,7 @@ class Delete : MagiskQueryBuilder {
} }
override fun toString(): String { override fun toString(): String {
return StringBuilder() return listOf(requestType, table, condition).joinToString(" ")
.appendln(requestType)
.appendln(table)
.appendln(condition)
.toString()
} }
} }
@@ -65,13 +61,7 @@ class Select : MagiskQueryBuilder {
} }
override fun toString(): String { override fun toString(): String {
return StringBuilder() return listOf(requestType, table, condition, orderField).joinToString(" ")
.appendln(requestType)
.appendln(table)
.appendln(condition)
.appendln(orderField)
.toString()
.replace("\n", " ")
} }
} }
@@ -84,23 +74,25 @@ open class Insert : MagiskQueryBuilder {
override lateinit var table: String override lateinit var table: String
private val keys get() = _values.keys.joinToString(",") private val keys get() = _values.keys.joinToString(",")
private val values get() = _values.values.joinToString(",") { "\"$it\"" } private val values get() = _values.values.joinToString(",") {
private var _values: Map<String, String> = mapOf() when (it) {
is Boolean -> if (it) "1" else "0"
is Number -> it.toString()
else -> "\"$it\""
}
}
private var _values: Map<String, Any> = mapOf()
fun values(vararg pairs: Pair<String, String>) { fun values(vararg pairs: Pair<String, Any>) {
_values = pairs.toMap() _values = pairs.toMap()
} }
fun values(values: Map<String, String>) { fun values(values: Map<String, Any>) {
_values = values _values = values
} }
override fun toString(): String { override fun toString(): String {
return StringBuilder() return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
.appendln(requestType)
.appendln(table)
.appendln("($keys) VALUES($values)")
.toString()
} }
} }

View File

@@ -1,63 +0,0 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.KConfig
import com.topjohnwu.magisk.model.entity.MagiskConfig
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Streaming
import retrofit2.http.Url
interface GithubRawApiServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/master/canary_builds/release.json")
fun fetchCanaryConfig(): Single<MagiskConfig>
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
fun fetchCanaryDebugConfig(): Single<MagiskConfig>
@GET
fun fetchCustomConfig(@Url url: String): Single<MagiskConfig>
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
@Streaming
fun fetchSafetynet(@Path(REVISION) revision: String = Constants.SNET_REVISION): Single<ResponseBody>
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
@Streaming
fun fetchBootctl(@Path(REVISION) revision: String = Constants.BOOTCTL_REVISION): Single<ResponseBody>
//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
fun fetchFile(@Url url: String): Single<ResponseBody>
companion object {
private const val REVISION = "revision"
private const val MODULE = "module"
private const val FILE = "file"
private const val MAGISK_FILES = KConfig.DEFAULT_CHANNEL
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
}
}

View File

@@ -0,0 +1,81 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.model.entity.UpdateInfo
import com.topjohnwu.magisk.tasks.GithubRepoInfo
import io.reactivex.Flowable
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.adapter.rxjava2.Result
import retrofit2.http.*
interface GithubRawServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchStableUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/canary/release.json")
fun fetchCanaryUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/canary/debug.json")
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
@GET
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
@Streaming
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
@Streaming
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
@Streaming
fun fetchInstaller(): Single<ResponseBody>
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<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
fun fetchFile(@Url url: String): Single<ResponseBody>
@GET
fun fetchString(@Url url: String): Single<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")
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): Flowable<Result<List<GithubRepoInfo>>>
}

View File

@@ -1,15 +0,0 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.model.entity.MagiskPolicy
class AppRepository(private val policyDao: PolicyDao) {
fun deleteOutdated() = policyDao.deleteOutdated()
fun delete(packageName: String) = policyDao.delete(packageName)
fun delete(uid: Int) = policyDao.delete(uid)
fun fetch(uid: Int) = policyDao.fetch(uid)
fun fetchAll() = policyDao.fetchAll()
fun update(policy: MagiskPolicy) = policyDao.update(policy)
}

View File

@@ -0,0 +1,106 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import io.reactivex.schedulers.Schedulers
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
interface DBConfig {
val settingsDao: SettingsDao
val stringDao: StringDao
fun dbSettings(
name: String,
default: Int
) = DBSettingsValue(name, default)
fun dbSettings(
name: String,
default: Boolean
) = DBBoolSettings(name, default)
fun dbStrings(
name: String,
default: String,
sync: Boolean = false
) = DBStringsValue(name, default, sync)
}
class DBSettingsValue(
private val name: String,
private val default: Int
) : ReadWriteProperty<DBConfig, Int> {
private var value: Int? = null
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null)
value = thisRef.settingsDao.fetch(name, default).blockingGet()
return value!!
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
synchronized(this) {
this.value = value
}
thisRef.settingsDao.put(name, value)
.subscribeOn(Schedulers.io())
.subscribe()
}
}
class DBBoolSettings(
name: String,
default: Boolean
) : ReadWriteProperty<DBConfig, Boolean> {
val base = DBSettingsValue(name, if (default) 1 else 0)
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
= base.getValue(thisRef, property) != 0
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
base.setValue(thisRef, property, if (value) 1 else 0)
}
class DBStringsValue(
private val name: String,
private val default: String,
private val sync: Boolean
) : ReadWriteProperty<DBConfig, String> {
private var value: String? = null
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
if (value == null)
value = thisRef.stringDao.fetch(name, default).blockingGet()
return value!!
}
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
synchronized(this) {
this.value = value
}
if (value.isEmpty()) {
if (sync) {
thisRef.stringDao.delete(name).blockingAwait()
} else {
thisRef.stringDao.delete(name)
.subscribeOn(Schedulers.io())
.subscribe()
}
} else {
if (sync) {
thisRef.stringDao.put(name, value).blockingAwait()
} else {
thisRef.stringDao.put(name, value)
.subscribeOn(Schedulers.io())
.subscribe()
}
}
}
}

View File

@@ -1,14 +1,12 @@
package com.topjohnwu.magisk.data.repository package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.Const import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.database.LogDao import com.topjohnwu.magisk.data.database.LogDao
import com.topjohnwu.magisk.data.database.base.suRaw import com.topjohnwu.magisk.data.database.base.suRaw
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -20,9 +18,8 @@ class LogRepository(
.map { it.sortByDescending { it.date.time }; it } .map { it.sortByDescending { it.date.time }; it }
.map { it.wrap() } .map { it.wrap() }
fun fetchMagiskLogs() = "tail -n 5000 ${Constants.MAGISK_LOG}".suRaw() fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.map { Timber.i(it.toString()); it }
fun clearLogs() = logDao.deleteAll() fun clearLogs() = logDao.deleteAll()
fun clearOutdated() = logDao.deleteOutdated() fun clearOutdated() = logDao.deleteOutdated()

View File

@@ -1,86 +1,49 @@
package com.topjohnwu.magisk.data.repository package com.topjohnwu.magisk.data.repository
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Config import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.KConfig import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.database.base.suRaw import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.data.network.GithubRawApiServices import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.model.entity.HideAppInfo import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.Version
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.writeToFile
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.BiFunction
class MagiskRepository( class MagiskRepository(
private val context: Context, private val apiRaw: GithubRawServices,
private val apiRaw: GithubRawApiServices, private val packageManager: PackageManager
private val packageManager: PackageManager
) { ) {
fun fetchMagisk() = fetchConfig() fun fetchSafetynet() = apiRaw.fetchSafetynet()
.flatMap { apiRaw.fetchFile(it.magisk.link) }
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
fun fetchManager() = fetchConfig() fun fetchUpdate() = when (Config.updateChannel) {
.flatMap { apiRaw.fetchFile(it.app.link) } Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
.map { it.writeToFile(context, FILE_MAGISK_APK) } Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
fun fetchUninstaller() = fetchConfig() Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
.flatMap { apiRaw.fetchFile(it.uninstaller.link) } Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) } else -> throw IllegalArgumentException()
}.flatMap {
fun fetchSafetynet() = apiRaw // If remote version is lower than current installed, try switching to beta
.fetchSafetynet() if (it.magisk.versionCode < Info.magiskVersionCode
.map { it.writeToFile(context, FILE_SAFETY_NET_APK) } && Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
fun fetchBootctl() = apiRaw apiRaw.fetchBetaUpdate()
.fetchBootctl() } else {
.map { it.writeToFile(context, FILE_BOOTCTL_SH) } Single.just(it)
fun fetchConfig() = when (KConfig.updateChannel) {
KConfig.UpdateChannel.STABLE -> apiRaw.fetchConfig()
KConfig.UpdateChannel.BETA -> apiRaw.fetchBetaConfig()
KConfig.UpdateChannel.CANARY -> apiRaw.fetchCanaryConfig()
KConfig.UpdateChannel.CANARY_DEBUG -> apiRaw.fetchCanaryDebugConfig()
KConfig.UpdateChannel.CUSTOM -> apiRaw.fetchCustomConfig(KConfig.customUpdateChannel)
}
.doOnSuccess {
Config.remoteMagiskVersionCode = it.magisk.versionCode.toIntOrNull() ?: -1
Config.magiskLink = it.magisk.link
Config.magiskNoteLink = it.magisk.note
Config.magiskMD5 = it.magisk.hash
Config.remoteManagerVersionCode = it.app.versionCode.toIntOrNull() ?: -1
Config.remoteManagerVersionString = it.app.version
Config.managerLink = it.app.link
Config.managerNoteLink = it.app.note
Config.uninstallerLink = it.uninstaller.link
}
fun fetchMagiskVersion(): Single<Version> = Single.zip(
fetchMagiskVersionName(),
fetchMagiskVersionCode(),
BiFunction { versionName, versionCode ->
Version(versionName, versionCode)
} }
) }.doOnSuccess { Info.remote = it }
fun fetchApps() = fun fetchApps() =
Single.fromCallable { packageManager.getInstalledApplications(0) } Single.fromCallable { packageManager.getInstalledApplications(0) }
.flattenAsFlowable { it } .flattenAsFlowable { it }
.filter { it.enabled && !blacklist.contains(it.packageName) } .filter { it.enabled && !blacklist.contains(it.packageName) }
.map { .map {
val label = Utils.getAppLabel(it, packageManager) val label = it.getLabel(packageManager)
val icon = it.loadIcon(packageManager) val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon) HideAppInfo(it, label, icon)
} }
@@ -93,30 +56,14 @@ class MagiskRepository(
.map { HideTarget(it) } .map { HideTarget(it) }
.toList() .toList()
private fun fetchMagiskVersionName() = "magisk -v".suRaw()
.map { it.first() }
.map { it.substring(0 until it.indexOf(":")) }
.onErrorReturn { "Unknown" }
private fun fetchMagiskVersionCode() = "magisk -V".suRaw()
.map { it.first() }
.map { it.toIntOrNull() ?: -1 }
.onErrorReturn { -1 }
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) = fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement() "magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
private val Boolean.state get() = if (this) "add" else "rm" private val Boolean.state get() = if (this) "add" else "rm"
companion object { companion object {
const val FILE_MAGISK_ZIP = "magisk.zip" private val blacklist by lazy { listOf(
const val FILE_MAGISK_APK = "magisk.apk" packageName,
const val FILE_UNINSTALLER_ZIP = "uninstaller.zip"
const val FILE_SAFETY_NET_APK = "safetynet.apk"
const val FILE_BOOTCTL_SH = "bootctl"
private val blacklist = listOf(
let { val app: App by inject(); app }.packageName,
"android", "android",
"com.android.chrome", "com.android.chrome",
"com.chrome.beta", "com.chrome.beta",
@@ -124,7 +71,7 @@ class MagiskRepository(
"com.chrome.canary", "com.chrome.canary",
"com.android.webview", "com.android.webview",
"com.google.android.webview" "com.google.android.webview"
) ) }
} }
} }

View File

@@ -1,11 +0,0 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao
class SettingRepository(private val settingsDao: SettingsDao) {
fun fetch(key: String, default: Int) = settingsDao.fetch(key, default)
fun put(key: String, value: Int) = settingsDao.put(key, value)
fun delete(key: String) = settingsDao.delete(key)
}

View File

@@ -1,11 +1,15 @@
package com.topjohnwu.magisk.data.repository package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.StringDao import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.model.entity.module.Repo
class StringRepository(private val stringDao: StringDao) { class StringRepository(
private val api: GithubRawServices
) {
fun fetch(key: String, default: String) = stringDao.fetch(key, default) fun getString(url: String) = api.fetchString(url)
fun put(key: String, value: String) = stringDao.put(key, value)
fun delete(key: String) = stringDao.delete(key)
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
} }

View File

@@ -0,0 +1,26 @@
package com.topjohnwu.magisk.databinding
import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.databinding.BindingAdapter
@BindingAdapter("gone")
fun setGone(view: View, gone: Boolean) {
view.isGone = gone
}
@BindingAdapter("invisible")
fun setInvisible(view: View, invisible: Boolean) {
view.isInvisible = invisible
}
@BindingAdapter("goneUnless")
fun setGoneUnless(view: View, goneUnless: Boolean) {
setGone(view, goneUnless.not())
}
@BindingAdapter("invisibleUnless")
fun setInvisibleUnless(view: View, invisibleUnless: Boolean) {
setInvisible(view, invisibleUnless.not())
}

View File

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

View File

@@ -0,0 +1,13 @@
package com.topjohnwu.magisk.databinding
import androidx.databinding.ViewDataBinding
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
open class BindingBoundAdapter : BindingRecyclerViewAdapter<RvItem>() {
override fun onBindBinding(binding: ViewDataBinding, variableId: Int, layoutRes: Int, position: Int, item: RvItem) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
item.onBindingBound(binding)
}
}

View File

@@ -0,0 +1,48 @@
package com.topjohnwu.magisk.databinding
import androidx.annotation.CallSuper
import androidx.databinding.ViewDataBinding
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.utils.DiffObservableList
import me.tatarka.bindingcollectionadapter2.ItemBinding
abstract class RvItem {
abstract val layoutRes: Int
@CallSuper
open fun bind(binding: ItemBinding<*>) {
binding.set(BR.item, layoutRes)
}
/**
* This callback is useful if you want to manipulate your views directly.
* If you want to use this callback, you must set [me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter]
* on your RecyclerView and call it from there. You can use [BindingBoundAdapter] for your convenience.
*/
open fun onBindingBound(binding: ViewDataBinding) {}
}
abstract class ComparableRvItem<in T> : RvItem() {
abstract fun itemSameAs(other: T): Boolean
abstract fun contentSameAs(other: T): Boolean
@Suppress("UNCHECKED_CAST")
open fun genericItemSameAs(other: Any): Boolean = other::class == this::class && itemSameAs(other as T)
@Suppress("UNCHECKED_CAST")
open fun genericContentSameAs(other: Any): Boolean = other::class == this::class && contentSameAs(other as T)
companion object {
val callback = object : DiffObservableList.Callback<ComparableRvItem<*>> {
override fun areItemsTheSame(
oldItem: ComparableRvItem<*>,
newItem: ComparableRvItem<*>
) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(
oldItem: ComparableRvItem<*>,
newItem: ComparableRvItem<*>
) = oldItem.genericContentSameAs(newItem)
}
}
}

View File

@@ -1,18 +1,61 @@
package com.topjohnwu.magisk.di package com.topjohnwu.magisk.di
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.skoumal.teanity.rxbus.RxBus import com.topjohnwu.magisk.utils.RxBus
import com.topjohnwu.magisk.App import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val SUTimeout = named("su_timeout")
val Protected = named("protected")
val applicationModule = module { val applicationModule = module {
single { RxBus() } single { RxBus() }
factory { get<Context>().resources } factory { get<Context>().resources }
factory { get<Context>() as App }
factory { get<Context>().packageManager } factory { get<Context>().packageManager }
factory(Protected) { get<App>().protectedContext } factory(Protected) { createDEContext(get()) }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) } single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) } single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
} single { ActivityTracker() }
factory { get<ActivityTracker>().foreground ?: NullActivity }
}
private fun createDEContext(context: Context): Context {
return if (Build.VERSION.SDK_INT >= 24)
context.createDeviceProtectedStorageContext()
else context
}
class ActivityTracker : Application.ActivityLifecycleCallbacks {
@Volatile
var foreground: Activity? = null
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
@Synchronized
override fun onActivityResumed(activity: Activity) {
foreground = activity
}
@Synchronized
override fun onActivityPaused(activity: Activity) {
foreground = null
}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}
@SuppressLint("Registered")
object NullActivity : Activity()

View File

@@ -1,7 +1,9 @@
package com.topjohnwu.magisk.di package com.topjohnwu.magisk.di
import android.content.Context
import androidx.room.Room
import com.topjohnwu.magisk.data.database.* import com.topjohnwu.magisk.data.database.*
import com.topjohnwu.magisk.tasks.UpdateRepos import com.topjohnwu.magisk.tasks.RepoUpdater
import org.koin.dsl.module import org.koin.dsl.module
@@ -10,6 +12,12 @@ val databaseModule = module {
single { PolicyDao(get()) } single { PolicyDao(get()) }
single { SettingsDao() } single { SettingsDao() }
single { StringDao() } single { StringDao() }
single { RepoDatabaseHelper(get()) } single { createRepoDatabase(get()) }
single { UpdateRepos(get()) } single { get<RepoDatabase>().repoDao() }
single { RepoUpdater(get(), get()) }
} }
fun createRepoDatabase(context: Context) =
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
.fallbackToDestructiveMigration()
.build()

View File

@@ -1,10 +0,0 @@
package com.topjohnwu.magisk.di
import org.koin.dsl.module
val miscModule = module {
// define miscs here
}

View File

@@ -5,6 +5,5 @@ val koinModules = listOf(
networkingModule, networkingModule,
databaseModule, databaseModule,
repositoryModule, repositoryModule,
viewModelModules, viewModelModules
miscModule
) )

View File

@@ -1,6 +0,0 @@
package com.topjohnwu.magisk.di
import org.koin.core.qualifier.named
val SUTimeout = named("su_timeout")
val Protected = named("protected")

View File

@@ -1,69 +1,83 @@
package com.topjohnwu.magisk.di package com.topjohnwu.magisk.di
import android.content.Context
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.Constants import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.data.network.GithubRawApiServices import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module import org.koin.dsl.module
import retrofit2.CallAdapter
import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import se.ansman.kotshi.KotshiJsonAdapterFactory import se.ansman.kotshi.KotshiJsonAdapterFactory
val networkingModule = module { val networkingModule = module {
single { createOkHttpClient() } single { createOkHttpClient(get()) }
single { createConverterFactory() } single { createRetrofit(get()) }
single { createCallAdapterFactory() } single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
single { createRetrofit(get(), get(), get()) } single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
single { createApiService<GithubRawApiServices>(get(), Constants.GITHUB_RAW_API_URL) } single { createMarkwon(get(), get()) }
} }
fun createOkHttpClient(): OkHttpClient { @Suppress("DEPRECATION")
val httpLoggingInterceptor = HttpLoggingInterceptor().apply { fun createOkHttpClient(context: Context): OkHttpClient {
level = HttpLoggingInterceptor.Level.BODY val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.HEADERS
}
builder.addInterceptor(httpLoggingInterceptor)
} }
return OkHttpClient.Builder() if (!Networking.init(context)) {
.addInterceptor(httpLoggingInterceptor) builder.sslSocketFactory(NoSSLv3SocketFactory())
.build() }
return builder.build()
} }
fun createConverterFactory(): Converter.Factory { fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder() val moshi = Moshi.Builder()
.add(JsonAdapterFactory.INSTANCE) .add(KotshiJsonAdapterFactory)
.build() .build()
return MoshiConverterFactory.create(moshi) return MoshiConverterFactory.create(moshi)
} }
fun createCallAdapterFactory(): CallAdapter.Factory { fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
return RxJava2CallAdapterFactory.create()
}
fun createRetrofit(
okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
callAdapterFactory: CallAdapter.Factory
): Retrofit.Builder {
return Retrofit.Builder() return Retrofit.Builder()
.addConverterFactory(converterFactory) .addConverterFactory(ScalarsConverterFactory.create())
.addCallAdapterFactory(callAdapterFactory) .addConverterFactory(createMoshiConverterFactory())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient) .client(okHttpClient)
} }
@KotshiJsonAdapterFactory @KotshiJsonAdapterFactory
abstract class JsonAdapterFactory : JsonAdapter.Factory { abstract class JsonAdapterFactory : JsonAdapter.Factory
companion object {
val INSTANCE: JsonAdapterFactory = KotshiJsonAdapterFactory
}
}
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T { inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
return retrofitBuilder return retrofitBuilder
.baseUrl(baseUrl) .baseUrl(baseUrl)
.build() .build()
.create(T::class.java) .create(T::class.java)
} }
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
return Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create {
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
})
.build()
}

View File

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

View File

@@ -20,6 +20,8 @@ val viewModelModules = module {
viewModel { HideViewModel(get(), get()) } viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get(), get()) } viewModel { ModuleViewModel(get(), get(), get()) }
viewModel { LogViewModel(get(), get()) } viewModel { LogViewModel(get(), get()) }
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) } viewModel { (action: String, file: Uri, additional: Uri) ->
FlashViewModel(action, file, additional, get())
}
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) } viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
} }

View File

@@ -0,0 +1,57 @@
package com.topjohnwu.magisk.extensions
import androidx.databinding.Observable
import androidx.databinding.ObservableBoolean
import androidx.databinding.ObservableField
import androidx.databinding.ObservableInt
fun <T> ObservableField<T>.addOnPropertyChangedCallback(
removeAfterChanged: Boolean = false,
callback: (T?) -> Unit
) {
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
callback(get())
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
}
})
}
fun ObservableInt.addOnPropertyChangedCallback(
removeAfterChanged: Boolean = false,
callback: (Int) -> Unit
) {
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
callback(get())
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
}
})
}
fun ObservableBoolean.addOnPropertyChangedCallback(
removeAfterChanged: Boolean = false,
callback: (Boolean) -> Unit
) {
addOnPropertyChangedCallback(object : Observable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: Observable?, propertyId: Int) {
callback(get())
if (removeAfterChanged) removeOnPropertyChangedCallback(this)
}
})
}
inline fun <T> ObservableField<T>.update(block: (T?) -> Unit) {
set(get().apply(block))
}
inline fun <T> ObservableField<T>.updateNonNull(block: (T) -> Unit) {
update {
it ?: return@update
block(it)
}
}
inline fun ObservableInt.update(block: (Int) -> Unit) {
set(get().apply(block))
}

View File

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

View File

@@ -0,0 +1,6 @@
package com.topjohnwu.magisk.extensions
import android.os.Handler
import android.os.Looper
fun ui(body: () -> Unit) = Handler(Looper.getMainLooper()).post(body)

View File

@@ -0,0 +1,201 @@
package com.topjohnwu.magisk.extensions
import androidx.databinding.ObservableField
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.*
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposables
import io.reactivex.functions.BiFunction
import io.reactivex.schedulers.Schedulers
import androidx.databinding.Observable as BindingObservable
fun <T> Observable<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Observable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun <T> Flowable<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Flowable<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun <T> Single<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Single<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun <T> Maybe<T>.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Maybe<T> = this.subscribeOn(subscribeOn).observeOn(observeOn)
fun Completable.applySchedulers(
subscribeOn: Scheduler = Schedulers.io(),
observeOn: Scheduler = AndroidSchedulers.mainThread()
): Completable = this.subscribeOn(subscribeOn).observeOn(observeOn)
/*=== ALIASES FOR OBSERVABLES ===*/
typealias OnCompleteListener = () -> Unit
typealias OnSuccessListener<T> = (T) -> Unit
typealias OnErrorListener = (Throwable) -> Unit
/*=== ALIASES FOR OBSERVABLES ===*/
fun <T> Observable<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun <T> Single<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError)
fun <T> Maybe<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun <T> Flowable<T>.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {},
onNext: OnSuccessListener<T> = {}
) = applySchedulers()
.subscribe(onNext, onError, onComplete)
fun Completable.subscribeK(
onError: OnErrorListener = { it.printStackTrace() },
onComplete: OnCompleteListener = {}
) = applySchedulers()
.subscribe(onComplete, onError)
fun <T> Observable<out T>.updateBy(
field: KObservableField<T?>
) = doOnNextUi { field.value = it }
.doOnErrorUi { field.value = null }
fun <T> Single<out T>.updateBy(
field: KObservableField<T?>
) = doOnSuccessUi { field.value = it }
.doOnErrorUi { field.value = null }
fun <T> Maybe<out T>.updateBy(
field: KObservableField<T?>
) = doOnSuccessUi { field.value = it }
.doOnErrorUi { field.value = null }
.doOnComplete { field.value = field.value }
fun <T> Flowable<out T>.updateBy(
field: KObservableField<T?>
) = doOnNextUi { field.value = it }
.doOnErrorUi { field.value = null }
fun Completable.updateBy(
field: KObservableField<Boolean>
) = doOnCompleteUi { field.value = true }
.doOnErrorUi { field.value = false }
fun <T> Observable<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { ui { body() } }
fun <T> Single<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { ui { body() } }
fun <T> Maybe<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { ui { body() } }
fun <T> Flowable<T>.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { ui { body() } }
fun Completable.doOnSubscribeUi(body: () -> Unit) =
doOnSubscribe { ui { body() } }
fun <T> Observable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { ui { body(it) } }
fun <T> Single<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { ui { body(it) } }
fun <T> Maybe<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { ui { body(it) } }
fun <T> Flowable<T>.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { ui { body(it) } }
fun Completable.doOnErrorUi(body: (Throwable) -> Unit) =
doOnError { ui { body(it) } }
fun <T> Observable<T>.doOnNextUi(body: (T) -> Unit) =
doOnNext { ui { body(it) } }
fun <T> Flowable<T>.doOnNextUi(body: (T) -> Unit) =
doOnNext { ui { body(it) } }
fun <T> Single<T>.doOnSuccessUi(body: (T) -> Unit) =
doOnSuccess { ui { body(it) } }
fun <T> Maybe<T>.doOnSuccessUi(body: (T) -> Unit) =
doOnSuccess { ui { body(it) } }
fun <T> Maybe<T>.doOnCompleteUi(body: () -> Unit) =
doOnComplete { ui { body() } }
fun Completable.doOnCompleteUi(body: () -> Unit) =
doOnComplete { ui { body() } }
fun <T, R> Observable<List<T>>.mapList(
transformer: (T) -> R
) = flatMapIterable { it }
.map(transformer)
.toList()
fun <T, R> Single<List<T>>.mapList(
transformer: (T) -> R
) = flattenAsFlowable { it }
.map(transformer)
.toList()
fun <T, R> Maybe<List<T>>.mapList(
transformer: (T) -> R
) = flattenAsFlowable { it }
.map(transformer)
.toList()
fun <T, R> Flowable<List<T>>.mapList(
transformer: (T) -> R
) = flatMapIterable { it }
.map(transformer)
.toList()
fun <T> ObservableField<T>.toObservable(): Observable<T> {
val observableField = this
return Observable.create { emitter ->
observableField.get()?.let { emitter.onNext(it) }
val callback = object : BindingObservable.OnPropertyChangedCallback() {
override fun onPropertyChanged(sender: BindingObservable?, propertyId: Int) {
observableField.get()?.let { emitter.onNext(it) }
}
}
observableField.addOnPropertyChangedCallback(callback)
emitter.setDisposable(Disposables.fromAction {
observableField.removeOnPropertyChangedCallback(callback)
})
}
}
fun <T : Any> T.toSingle() = Single.just(this)
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })

View File

@@ -0,0 +1,126 @@
package com.topjohnwu.magisk.extensions
import android.content.Context
import android.content.res.ColorStateList
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import com.google.android.material.snackbar.Snackbar
fun AppCompatActivity.snackbar(
view: View,
@StringRes messageRes: Int,
length: Int = Snackbar.LENGTH_SHORT,
f: Snackbar.() -> Unit = {}
) {
snackbar(view, getString(messageRes), length, f)
}
fun AppCompatActivity.snackbar(
view: View,
message: String,
length: Int = Snackbar.LENGTH_SHORT,
f: Snackbar.() -> Unit = {}
) = Snackbar.make(view, message, length)
.apply(f)
.show()
fun Fragment.snackbar(
view: View,
@StringRes messageRes: Int,
length: Int = Snackbar.LENGTH_SHORT,
f: Snackbar.() -> Unit = {}
) {
snackbar(view, getString(messageRes), length, f)
}
fun Fragment.snackbar(
view: View,
message: String,
length: Int = Snackbar.LENGTH_SHORT,
f: Snackbar.() -> Unit = {}
) = Snackbar.make(view, message, length)
.apply(f)
.show()
fun Snackbar.action(init: KSnackbar.() -> Unit) = apply {
val config = KSnackbar().apply(init)
setAction(config.title(context), config.onClickListener)
when {
config.hasValidColor -> setActionTextColor(config.color(context) ?: return@apply)
config.hasValidColorStateList -> setActionTextColor(config.colorStateList(context) ?: return@apply)
}
}
class KSnackbar {
var colorRes: Int = -1
var colorStateListRes: Int = -1
var title: CharSequence = ""
var titleRes: Int = -1
internal var onClickListener: (View) -> Unit = {}
internal val hasValidColor get() = colorRes != -1
internal val hasValidColorStateList get() = colorStateListRes != -1
fun onClicked(listener: (View) -> Unit) {
onClickListener = listener
}
internal fun title(context: Context) = if (title.isBlank()) context.getString(titleRes) else title
internal fun colorStateList(context: Context) = context.colorStateListCompat(colorStateListRes)
internal fun color(context: Context) = context.colorCompat(colorRes)
}
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
fun Snackbar.action(
@StringRes actionRes: Int,
@ColorRes colorRes: Int? = null,
listener: (View) -> Unit
) {
view.resources.getString(actionRes)
colorRes?.let { ContextCompat.getColor(view.context, colorRes) }
action {}
}
@Deprecated("Kotlin DSL version is preferred", ReplaceWith("action {}"))
fun Snackbar.action(action: String, @ColorInt color: Int? = null, listener: (View) -> Unit) {
setAction(action, listener)
color?.let { setActionTextColor(color) }
}
fun Snackbar.textColorRes(@ColorRes colorRes: Int) {
textColor(context.colorCompat(colorRes) ?: return)
}
fun Snackbar.textColor(@ColorInt color: Int) {
val tv = view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(color)
}
fun Snackbar.backgroundColorRes(@ColorRes colorRes: Int) {
backgroundColor(context.colorCompat(colorRes) ?: return)
}
fun Snackbar.backgroundColor(@ColorInt color: Int) {
ViewCompat.setBackgroundTintList(
view,
ColorStateList.valueOf(color)
)
}
fun Snackbar.alert() {
textColor(0xF44336)
}
fun Snackbar.success() {
textColor(0x4CAF50)
}

View File

@@ -0,0 +1,159 @@
package com.topjohnwu.magisk.extensions
import android.content.Context
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.res.Configuration
import android.content.res.Resources
import android.database.Cursor
import android.net.Uri
import android.os.Build
import android.provider.OpenableColumns
import android.view.View
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.topjohnwu.magisk.utils.FileProvider
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.currentLocale
import java.io.File
import java.io.FileNotFoundException
val packageName: String get() = get<Context>().packageName
val PackageInfo.processes
get() = activities?.processNames.orEmpty() +
services?.processNames.orEmpty() +
receivers?.processNames.orEmpty() +
providers?.processNames.orEmpty()
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
val ApplicationInfo.packageInfo: PackageInfo?
get() {
val pm: PackageManager by inject()
return try {
val request = GET_ACTIVITIES or
GET_SERVICES or
GET_RECEIVERS or
GET_PROVIDERS
pm.getPackageInfo(packageName, request)
} catch (e1: Exception) {
try {
pm.activities(packageName).apply {
services = pm.services(packageName)
receivers = pm.receivers(packageName)
providers = pm.providers(packageName)
}
} catch (e2: Exception) {
null
}
}
}
val Uri.fileName: String
get() {
var name: String? = null
get<Context>().contentResolver.query(this, null, null, null, null)?.use { c ->
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1) {
c.moveToFirst()
name = c.getString(nameIndex)
}
}
if (name == null && path != null) {
val idx = path!!.lastIndexOf('/')
name = path!!.substring(idx + 1)
}
return name.orEmpty()
}
fun PackageManager.activities(packageName: String) =
getPackageInfo(packageName, GET_ACTIVITIES)
fun PackageManager.services(packageName: String) =
getPackageInfo(packageName, GET_SERVICES).services
fun PackageManager.receivers(packageName: String) =
getPackageInfo(packageName, GET_RECEIVERS).receivers
fun PackageManager.providers(packageName: String) =
getPackageInfo(packageName, GET_PROVIDERS).providers
fun Context.rawResource(id: Int) = resources.openRawResource(id)
fun Context.readUri(uri: Uri) =
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun File.provide(context: Context = get()): Uri {
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
}
fun File.mv(destination: File) {
inputStream().writeTo(destination)
deleteRecursively()
}
fun String.toFile() = File(this)
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
fun Context.cachedFile(name: String) = File(cacheDir, name)
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
val out = mutableListOf<Result>()
while (moveToNext()) out.add(transformer(this))
return out
}
fun ApplicationInfo.getLabel(pm: PackageManager): String {
runCatching {
if (labelRes > 0) {
val res = pm.getResourcesForApplication(this)
val config = Configuration()
config.setLocale(currentLocale)
res.updateConfiguration(config, res.displayMetrics)
return res.getString(labelRes)
}
}
return loadLabel(pm).toString()
}
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
fun Context.colorCompat(@ColorRes id: Int) = try {
ContextCompat.getColor(this, id)
} catch (e: Resources.NotFoundException) {
null
}
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
ContextCompat.getColorStateList(this, id)
} catch (e: Resources.NotFoundException) {
null
}
fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
) {
return end to start
}
return start to end
}
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())

View File

@@ -0,0 +1,8 @@
package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.utils.KObservableField
fun KObservableField<Boolean>.toggle() {
value = !value
}

View File

@@ -0,0 +1,103 @@
package com.topjohnwu.magisk.extensions
import android.net.Uri
import android.os.Build
import androidx.core.net.toFile
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import kotlin.NoSuchElementException
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
var entry: ZipEntry? = nextEntry
while (entry != null) {
callback(entry)
entry = nextEntry
}
}
fun Uri.writeTo(file: File) = toFile().copyTo(file)
fun InputStream.writeTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
inline fun <In : InputStream, Out : OutputStream> withStreams(
inStream: In,
outStream: Out,
withBoth: (In, Out) -> Unit
) {
inStream.use { reader ->
outStream.use { writer ->
withBoth(reader, writer)
}
}
}
inline fun <T, R> List<T>.firstMap(mapper: (T) -> R?): R {
for (item: T in this) {
return mapper(item) ?: continue
}
throw NoSuchElementException("Collection contains no element matching the predicate.")
}
fun String.langTagToLocale(): Locale {
if (Build.VERSION.SDK_INT >= 21) {
return Locale.forLanguageTag(this)
} else {
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (tok.isEmpty()) {
return Locale("")
}
val language = when (tok[0]) {
"und" -> "" // Undefined
"fil" -> "tl" // Filipino
else -> tok[0]
}
if (language.length != 2 && language.length != 3)
return Locale("")
if (tok.size == 1)
return Locale(language)
val country = tok[1]
return if (country.length != 2 && country.length != 3) Locale(language)
else Locale(language, country)
}
}
fun Locale.toLangTag(): String {
if (Build.VERSION.SDK_INT >= 21) {
return toLanguageTag()
} else {
var language = language
var country = country
var variant = variant
when {
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
language = "und" // Follow the Locale#toLanguageTag() implementation
language == "iw" -> language = "he" // correct deprecated "Hebrew"
language == "in" -> language = "id" // correct deprecated "Indonesian"
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
}
// ensure valid country code, if not well formed, it's omitted
// variant subtags that begin with a letter must be at least 5 characters long
// ensure valid country code, if not well formed, it's omitted
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
country = ""
}
// variant subtags that begin with a letter must be at least 5 characters long
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
variant = ""
}
val tag = StringBuilder(language)
if (country.isNotEmpty())
tag.append('-').append(country)
if (variant.isNotEmpty())
tag.append('-').append(variant)
return tag.toString()
}
}

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.extensions
import org.koin.core.context.GlobalContext import org.koin.core.context.GlobalContext
import org.koin.core.parameter.ParametersDefinition import org.koin.core.parameter.ParametersDefinition
@@ -6,12 +6,12 @@ import org.koin.core.qualifier.Qualifier
fun getKoin() = GlobalContext.get().koin fun getKoin() = GlobalContext.get().koin
inline fun <reified T : Any> inject( inline fun <reified T> inject(
qualifier: Qualifier? = null, qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null noinline parameters: ParametersDefinition? = null
) = lazy { get<T>(qualifier, parameters) } ) = lazy { get<T>(qualifier, parameters) }
inline fun <reified T : Any> get( inline fun <reified T> get(
qualifier: Qualifier? = null, qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null noinline parameters: ParametersDefinition? = null
): T = getKoin().get(qualifier, parameters) ): T = getKoin().get(qualifier, parameters)

View File

@@ -1,8 +1,8 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.extensions
import androidx.collection.SparseArrayCompat
import androidx.databinding.ObservableList import androidx.databinding.ObservableList
import com.skoumal.teanity.extensions.subscribeK import com.topjohnwu.magisk.utils.DiffObservableList
import com.skoumal.teanity.util.DiffObservableList
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
fun <T> MutableList<T>.update(newList: List<T>) { fun <T> MutableList<T>.update(newList: List<T>) {
@@ -25,8 +25,8 @@ fun List<String>.toShellCmd(): String {
} }
fun <T1, T2> ObservableList<T1>.sendUpdatesTo( fun <T1, T2> ObservableList<T1>.sendUpdatesTo(
target: DiffObservableList<T2>, target: DiffObservableList<T2>,
mapper: (List<T1>) -> List<T2> mapper: (List<T1>) -> List<T2>
) = addOnListChangedCallback(object : ) = addOnListChangedCallback(object :
ObservableList.OnListChangedCallback<ObservableList<T1>>() { ObservableList.OnListChangedCallback<ObservableList<T1>>() {
override fun onChanged(sender: ObservableList<T1>?) { override fun onChanged(sender: ObservableList<T1>?) {
@@ -76,4 +76,8 @@ fun <T1> ObservableList<T1>.copyNewInputInto(
val addedValues = sender?.slice(positionStart until positionEnd).orEmpty() val addedValues = sender?.slice(positionStart until positionEnd).orEmpty()
target.addAll(addedValues) target.addAll(addedValues)
} }
}) })
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
put(key, value)
}

View File

@@ -0,0 +1,14 @@
package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.Info
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import java.io.File
fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
}
fun File.suOutputStream() = SuFileOutputStream(this)
fun File.suInputStream() = SuFileInputStream(this)

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.extensions
import android.content.res.Resources import android.content.res.Resources
@@ -20,4 +20,8 @@ fun Int.res(vararg args: Any): String {
return resources.getString(this, *args) return resources.getString(this, *args)
} }
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
.replace("#", "").replace("@", "").replace("\\", "_")

View File

@@ -1,5 +1,6 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.utils.currentLocale
import java.text.DateFormat import java.text.DateFormat
import java.text.ParseException import java.text.ParseException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -13,8 +14,7 @@ fun String.toTime(format: DateFormat) = try {
-1L -1L
} }
private val locale get() = LocaleManager.locale val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", locale) } val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", locale) } val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale) } val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", LocaleManager.locale) }

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils package com.topjohnwu.magisk.extensions
import android.view.View import android.view.View
import android.view.ViewTreeObserver import android.view.ViewTreeObserver

View File

@@ -0,0 +1,37 @@
package com.topjohnwu.magisk.model.binding
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
class BindingAdapter : BindingRecyclerViewAdapter<ComparableRvItem<*>>() {
private var recyclerView: RecyclerView? = null
override fun onBindBinding(
binding: ViewDataBinding,
variableId: Int,
layoutRes: Int,
position: Int,
item: ComparableRvItem<*>
) {
super.onBindBinding(binding, variableId, layoutRes, position, item)
when (item) {
is LenientRvItem -> {
val recycler = recyclerView ?: return
item.onBindingBound(binding)
item.onBindingBound(binding, recycler)
}
else -> item.onBindingBound(binding)
}
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
this.recyclerView = recyclerView
}
}

View File

@@ -1,156 +0,0 @@
package com.topjohnwu.magisk.model.download;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.ui.flash.FlashActivity;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.magisk.view.ProgressNotification;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import androidx.annotation.Nullable;
public class DownloadModuleService extends Service {
private List<ProgressNotification> notifications;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
notifications = new ArrayList<>();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Shell.EXECUTOR.execute(() -> {
Repo repo = intent.getParcelableExtra("repo");
boolean install = intent.getBooleanExtra("install", false);
dlProcessInstall(repo, install);
});
return START_REDELIVER_INTENT;
}
@Override
public synchronized void onTaskRemoved(Intent rootIntent) {
for (ProgressNotification n : notifications) {
Notifications.mgr.cancel(n.hashCode());
}
notifications.clear();
}
private synchronized void addNotification(ProgressNotification n) {
if (notifications.isEmpty()) {
// Start foreground
startForeground(n.hashCode(), n.getNotification());
}
notifications.add(n);
}
private synchronized void removeNotification(ProgressNotification n) {
notifications.remove(n);
if (notifications.isEmpty()) {
// No more tasks, stop service
stopForeground(true);
stopSelf();
} else {
// Pick another notification as our foreground notification
n = notifications.get(0);
startForeground(n.hashCode(), n.getNotification());
}
}
private void dlProcessInstall(Repo repo, boolean install) {
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
ProgressNotification progress = new ProgressNotification(output.getName());
addNotification(progress);
try {
InputStream in = Networking.get(repo.getZipUrl())
.setDownloadProgressListener(progress)
.execForInputStream().getResult();
OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
processZip(in, out);
Intent intent = new Intent(this, ClassMap.get(FlashActivity.class));
intent.setData(Uri.fromFile(output))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
synchronized (getApplication()) {
if (install && App.foreground() != null &&
!(App.foreground() instanceof FlashActivity)) {
/* Only start flashing if there is a foreground activity and the
* user is not also flashing another module at the same time */
App.foreground().startActivity(intent);
} else {
/* Or else we preset a notification notifying that we are done */
PendingIntent pi = PendingIntent.getActivity(this, progress.hashCode(), intent,
PendingIntent.FLAG_UPDATE_CURRENT);
progress.dlDone(pi);
}
}
} catch (Exception e) {
e.printStackTrace();
progress.dlFail();
}
removeNotification(progress);
}
private void processZip(InputStream in, OutputStream out)
throws IOException {
try (ZipInputStream zin = new ZipInputStream(in);
ZipOutputStream zout = new ZipOutputStream(out)) {
// Inject latest module-installer.sh as update-binary
zout.putNextEntry(new ZipEntry("META-INF/"));
zout.putNextEntry(new ZipEntry("META-INF/com/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/"));
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/update-binary"));
try (InputStream update_bin = Networking.get(Const.Url.MODULE_INSTALLER)
.execForInputStream().getResult()) {
ShellUtils.pump(update_bin, zout);
}
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/updater-script"));
zout.write("#MAGISK\n".getBytes("UTF-8"));
int off = -1;
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
if (off < 0)
off = entry.getName().indexOf('/') + 1;
String path = entry.getName().substring(off);
if (path.isEmpty())
continue;
if (path.startsWith("META-INF"))
continue;
zout.putNextEntry(new ZipEntry(path));
if (!entry.isDirectory())
ShellUtils.pump(zin, zout);
}
}
}
}

View File

@@ -0,0 +1,155 @@
package com.topjohnwu.magisk.model.download
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists
import com.topjohnwu.magisk.extensions.provide
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.APKInstall
import org.koin.core.get
import java.io.File
import kotlin.random.Random.Default.nextInt
/* More of a facade for [RemoteFileService], but whatever... */
@SuppressLint("Registered")
open class DownloadService : RemoteFileService() {
private val context get() = this
private val File.type
get() = MimeTypeMap.getSingleton()
.getMimeTypeFromExtension(extension)
?: "resource/folder"
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
is Magisk -> onFinishedInternal(subject, id)
is Module -> onFinishedInternal(subject, id)
is Manager -> onFinishedInternal(subject, id)
}
private fun onFinishedInternal(
subject: Magisk,
id: Int
) = when (val conf = subject.configuration) {
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
else -> Unit
}
private fun onFinishedInternal(
subject: Module,
id: Int
) = when (subject.configuration) {
is Flash -> FlashActivity.install(this, subject.file, id)
else -> Unit
}
private fun onFinishedInternal(
subject: Manager,
id: Int
) {
remove(id)
when (subject.configuration) {
is APK.Upgrade -> APKInstall.install(this, subject.file)
else -> Unit
}
}
// ---
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
= when (subject) {
is Magisk -> addActionsInternal(subject)
is Module -> addActionsInternal(subject)
is Manager -> addActionsInternal(subject)
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
= when (val conf = subject.configuration) {
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
fileIntent(subject.file)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
}
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
else -> this
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
= when (subject.configuration) {
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
fileIntent(subject.file)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
}
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
else -> this
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
= when (subject.configuration) {
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
else -> this
}
@Suppress("ReplaceSingleLineLet")
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { setContentIntent(it) }
@Suppress("ReplaceSingleLineLet")
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { addAction(icon, getString(title), it) }
// ---
private fun fileIntent(file: File): Intent {
return Intent(Intent.ACTION_VIEW)
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
class Builder {
lateinit var subject: DownloadSubject
}
companion object {
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
val app = context.applicationContext
val builder = Builder().apply(argBuilder)
val intent = Intent(app, ClassMap[DownloadService::class.java])
.putExtra(ARG_URL, builder.subject)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
app.startForegroundService(intent)
} else {
app.startService(intent)
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.topjohnwu.magisk.model.download
import android.content.ComponentName
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.PatchAPK
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.superuser.Shell
import timber.log.Timber
import java.io.File
private fun RemoteFileService.patchPackage(apk: File, id: Int) {
if (packageName != BuildConfig.APPLICATION_ID) {
update(id) { notification ->
notification.setProgress(0, 0, true)
.setProgress(0, 0, true)
.setContentTitle(getString(R.string.hide_manager_title))
.setContentText("")
}
val patched = File(apk.parent, "patched.apk")
try {
// Try using the new APK to patch itself
val loader = DynamicClassLoader(apk)
loader.loadClass("a.a")
.getMethod("patchAPK", String::class.java, String::class.java, String::class.java)
.invoke(null, apk.path, patched.path, packageName)
} catch (e: Exception) {
Timber.e(e)
// Fallback to use the current implementation
PatchAPK.patch(apk.path, patched.path, packageName)
}
apk.delete()
patched.renameTo(apk)
}
}
private fun RemoteFileService.restore(apk: File, id: Int) {
update(id) { notification ->
notification.setProgress(0, 0, true)
.setProgress(0, 0, true)
.setContentTitle(getString(R.string.restore_img_msg))
.setContentText("")
}
Config.export()
// Make it world readable
apk.setReadable(true, false)
if (Shell.su("pm install $apk").exec().isSuccess) {
val component = ComponentName(BuildConfig.APPLICATION_ID,
ClassMap.get<Class<*>>(SplashActivity::class.java).name)
RootUtils.rmAndLaunch(packageName, component)
}
}
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
= when (subject.configuration) {
is Upgrade -> patchPackage(subject.file, subject.hashCode())
is Restore -> restore(subject.file, subject.hashCode())
}

View File

@@ -0,0 +1,44 @@
package com.topjohnwu.magisk.model.download
import com.topjohnwu.magisk.extensions.withStreams
import java.io.File
import java.io.InputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
fun InputStream.toModule(file: File, installer: InputStream) {
val input = ZipInputStream(buffered())
val output = ZipOutputStream(file.outputStream().buffered())
withStreams(input, output) { zin, zout ->
zout.putNextEntry(ZipEntry("META-INF/"))
zout.putNextEntry(ZipEntry("META-INF/com/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
installer.copyTo(zout)
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
var off = -1
var entry: ZipEntry? = zin.nextEntry
while (entry != null) {
if (off < 0) {
off = entry.name.indexOf('/') + 1
}
val path = entry.name.substring(off)
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
zout.putNextEntry(ZipEntry(path))
if (!entry.isDirectory) {
zin.copyTo(zout)
}
}
entry = zin.nextEntry
}
}
}

View File

@@ -0,0 +1,87 @@
package com.topjohnwu.magisk.model.download
import android.app.Notification
import android.app.Service
import android.content.Intent
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.koin.core.KoinComponent
import java.util.*
import kotlin.random.Random.Default.nextInt
abstract class NotificationService : Service(), KoinComponent {
abstract val defaultNotification: NotificationCompat.Builder
private val manager by lazy { NotificationManagerCompat.from(this) }
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
notifications.forEach { cancel(it.key) }
notifications.clear()
}
// --
fun update(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) { defaultNotification }
notify(id, notification.also(body).build())
if (notifications.size == 1) {
updateForeground()
}
}
protected fun finishNotify(
id: Int,
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
) : Int {
val currentNotification = remove(id)?.run(editBody)
var newId = -1
currentNotification?.let {
newId = nextInt(Int.MAX_VALUE)
notify(newId, it.build())
}
if (!hasNotifications) {
stopSelf()
}
return newId
}
// ---
private fun notify(id: Int, notification: Notification) {
manager.notify(id, notification)
}
private fun cancel(id: Int) {
manager.cancel(id)
}
protected fun remove(id: Int) = notifications.remove(id).also {
cancel(id)
updateForeground()
}
private fun updateForeground() {
if (hasNotifications)
startForeground(notifications.keys.first(), notifications.values.first().build())
else
stopForeground(true)
}
// --
override fun onBind(p0: Intent?): IBinder? = null
}

View File

@@ -0,0 +1,118 @@
package com.topjohnwu.magisk.model.download
import android.app.Activity
import android.content.Intent
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.NullActivity
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.utils.ProgressInputStream
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.ShellUtils
import io.reactivex.Completable
import okhttp3.ResponseBody
import org.koin.android.ext.android.inject
import timber.log.Timber
import java.io.InputStream
abstract class RemoteFileService : NotificationService() {
private val service: GithubRawServices by inject()
override val defaultNotification: NotificationCompat.Builder
get() = Notifications.progress(this, "")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
return START_REDELIVER_INTENT
}
// ---
private fun start(subject: DownloadSubject) = checkExisting(subject)
.onErrorResumeNext { download(subject) }
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
.subscribeK(onError = {
Timber.e(it)
finishNotify(subject.hashCode()) { notification ->
notification.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false)
}
}) {
val newId = finishNotify(subject)
if (get<Activity>() !is NullActivity) {
onFinished(subject, newId)
}
}
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
check(subject is Magisk) { "Download cache is disabled" }
subject.file.also {
check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) {
"The given file does not match checksum"
}
}
}
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
.map { it.toStream(subject.hashCode()) }
.flatMapCompletable { stream ->
when (subject) {
is Module -> service.fetchInstaller()
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
.ignoreElement()
else -> Completable.fromAction { stream.writeTo(subject.file) }
}
}.doOnComplete {
if (subject is Manager)
handleAPK(subject)
}
private fun ResponseBody.toStream(id: Int): InputStream {
val maxRaw = contentLength()
val max = maxRaw / 1_000_000f
return ProgressInputStream(byteStream()) {
val progress = it / 1_000_000f
update(id) { notification ->
if (maxRaw > 0) {
notification
.setProgress(maxRaw.toInt(), it.toInt(), false)
.setContentText("%.2f / %.2f MB".format(progress, max))
} else {
notification.setContentText("%.2f MB / ??".format(progress))
}
}
}
}
private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
it.addActions(subject)
.setContentText(getString(R.string.download_complete))
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
}
// ---
@Throws(Throwable::class)
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
: NotificationCompat.Builder
companion object {
const val ARG_URL = "arg_url"
}
}

View File

@@ -1,155 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
import androidx.annotation.NonNull;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;
private int mVersionCode = -1;
protected BaseModule() {
mId = mName = mVersion = mAuthor = mDescription = "";
}
protected BaseModule(Cursor c) {
mId = nonNull(c.getString(c.getColumnIndex("id")));
mName = nonNull(c.getString(c.getColumnIndex("name")));
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
}
protected BaseModule(Parcel p) {
mId = p.readString();
mName = p.readString();
mVersion = p.readString();
mAuthor = p.readString();
mDescription = p.readString();
mVersionCode = p.readInt();
}
protected BaseModule(MagiskModule m) {
mId = m.getId();
mName = m.getName();
mVersion = m.getVersion();
mAuthor = m.getAuthor();
mDescription = m.getDescription();
mVersionCode = Integer.parseInt(m.getVersionCode());
}
@Override
public int compareTo(@NonNull BaseModule module) {
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mId);
dest.writeString(mName);
dest.writeString(mVersion);
dest.writeString(mAuthor);
dest.writeString(mDescription);
dest.writeInt(mVersionCode);
}
private String nonNull(String s) {
return s == null ? "" : s;
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("id", mId);
values.put("name", mName);
values.put("version", mVersion);
values.put("versionCode", mVersionCode);
values.put("author", mAuthor);
values.put("description", mDescription);
return values;
}
protected void parseProps(List<String> props) {
parseProps(props.toArray(new String[0]));
}
protected void parseProps(String[] props) throws NumberFormatException {
for (String line : props) {
String[] prop = line.split("=", 2);
if (prop.length != 2)
continue;
String key = prop[0].trim();
String value = prop[1].trim();
if (key.isEmpty() || key.charAt(0) == '#')
continue;
switch (key) {
case "id":
mId = value;
break;
case "name":
mName = value;
break;
case "version":
mVersion = value;
break;
case "versionCode":
mVersionCode = Integer.parseInt(value);
break;
case "author":
mAuthor = value;
break;
case "description":
mDescription = value;
break;
default:
break;
}
}
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getVersion() {
return mVersion;
}
public String getAuthor() {
return mAuthor;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getDescription() {
return mDescription;
}
public int getVersionCode() {
return mVersionCode;
}
}

View File

@@ -2,8 +2,8 @@ package com.topjohnwu.magisk.model.entity
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.topjohnwu.magisk.utils.packageInfo import com.topjohnwu.magisk.extensions.packageInfo
import com.topjohnwu.magisk.utils.processes import com.topjohnwu.magisk.extensions.processes
class HideAppInfo( class HideAppInfo(
val info: ApplicationInfo, val info: ApplicationInfo,

View File

@@ -1,11 +0,0 @@
package com.topjohnwu.magisk.model.entity
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskApp(
val version: String,
val versionCode: String,
val link: String,
val note: String
)

View File

@@ -1,10 +0,0 @@
package com.topjohnwu.magisk.model.entity
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskConfig(
val app: MagiskApp,
val uninstaller: MagiskLink,
val magisk: MagiskFlashable
)

View File

@@ -1,13 +0,0 @@
package com.topjohnwu.magisk.model.entity
import com.squareup.moshi.Json
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskFlashable(
val version: String,
val versionCode: String,
val link: String,
val note: String,
@Json(name = "md5") val hash: String
)

View File

@@ -1,8 +0,0 @@
package com.topjohnwu.magisk.model.entity
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class MagiskLink(
val link: String
)

View File

@@ -1,8 +1,8 @@
package com.topjohnwu.magisk.model.entity package com.topjohnwu.magisk.model.entity
import com.topjohnwu.magisk.extensions.timeFormatTime
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
import com.topjohnwu.magisk.utils.timeFormatTime
import com.topjohnwu.magisk.utils.toTime
import java.util.* import java.util.*
data class MagiskLog( data class MagiskLog(
@@ -38,7 +38,6 @@ fun Map<String, String>.toLog(): MagiskLog {
fun Long.toDate() = Date(this) fun Long.toDate() = Date(this)
fun MagiskLog.toMap() = mapOf( fun MagiskLog.toMap() = mapOf(
"from_uid" to fromUid, "from_uid" to fromUid,
"to_uid" to toUid, "to_uid" to toUid,
@@ -47,8 +46,8 @@ fun MagiskLog.toMap() = mapOf(
"app_name" to appName, "app_name" to appName,
"command" to command, "command" to command,
"action" to action, "action" to action,
"time" to date "time" to date.time
).mapValues { it.toString() } )
fun MagiskPolicy.toLog( fun MagiskPolicy.toLog(
toUid: Int, toUid: Int,

View File

@@ -1,86 +0,0 @@
package com.topjohnwu.magisk.model.entity
import android.os.Parcelable
import androidx.annotation.AnyThread
import androidx.annotation.NonNull
import androidx.annotation.WorkerThread
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.Constants
import com.topjohnwu.magisk.data.database.base.su
import io.reactivex.Single
import kotlinx.android.parcel.Parcelize
import okhttp3.ResponseBody
import java.io.File
interface MagiskModule : Parcelable {
val id: String
val name: String
val author: String
val version: String
val versionCode: String
val description: String
}
@Entity(tableName = "repos")
@Parcelize
data class Repository(
@PrimaryKey @NonNull
override val id: String,
override val name: String,
override val author: String,
override val version: String,
override val versionCode: String,
override val description: String,
val lastUpdate: Long
) : MagiskModule
@Parcelize
data class Module(
override val id: String,
override val name: String,
override val author: String,
override val version: String,
override val versionCode: String,
override val description: String,
val path: String
) : MagiskModule
@AnyThread
fun File.toModule(): Single<Module> {
val path = "${Constants.MAGISK_PATH}/$name"
return "dos2unix < $path/module.prop".su()
.map { it.first().toModule(path) }
}
fun Map<String, String>.toModule(path: String): Module {
return Module(
id = get("id").orEmpty(),
name = get("name").orEmpty(),
author = get("author").orEmpty(),
version = get("version").orEmpty(),
versionCode = get("versionCode").orEmpty(),
description = get("description").orEmpty(),
path = path
)
}
@WorkerThread
fun ResponseBody.toRepository(lastUpdate: Long) = string()
.split(Regex("\\n"))
.map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()
.toRepository(lastUpdate)
@AnyThread
fun Map<String, String>.toRepository(lastUpdate: Long) = Repository(
id = get("id").orEmpty(),
name = get("name").orEmpty(),
author = get("author").orEmpty(),
version = get("version").orEmpty(),
versionCode = get("versionCode").orEmpty(),
description = get("description").orEmpty(),
lastUpdate = lastUpdate
)

View File

@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.entity
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
@@ -24,42 +25,14 @@ data class MagiskPolicy(
} }
/*@Throws(PackageManager.NameNotFoundException::class)
fun ContentValues.toPolicy(pm: PackageManager): MagiskPolicy {
val uid = getAsInteger("uid")
val packageName = getAsString("package_name")
val info = pm.getApplicationInfo(packageName, 0)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
return MagiskPolicy(
uid = uid,
packageName = packageName,
policy = getAsInteger("policy"),
until = getAsInteger("until").toLong(),
logging = getAsInteger("logging") != 0,
notification = getAsInteger("notification") != 0,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
)
}
fun MagiskPolicy.toContentValues() = ContentValues().apply {
put("uid", uid)
put("uid", uid)
put("package_name", packageName)
put("policy", policy)
put("until", until)
put("logging", if (logging) 1 else 0)
put("notification", if (notification) 1 else 0)
}*/
fun MagiskPolicy.toMap() = mapOf( fun MagiskPolicy.toMap() = mapOf(
"uid" to uid, "uid" to uid,
"package_name" to packageName, "package_name" to packageName,
"policy" to policy, "policy" to policy,
"until" to until, "until" to until,
"logging" to if (logging) 1 else 0, "logging" to logging,
"notification" to if (notification) 1 else 0 "notification" to notification
).mapValues { it.value.toString() } )
@Throws(PackageManager.NameNotFoundException::class) @Throws(PackageManager.NameNotFoundException::class)
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy { fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
@@ -78,7 +51,7 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
logging = get("logging")?.toIntOrNull() != 0, logging = get("logging")?.toIntOrNull() != 0,
notification = get("notification")?.toIntOrNull() != 0, notification = get("notification")?.toIntOrNull() != 0,
applicationInfo = info, applicationInfo = info,
appName = info.loadLabel(pm).toString() appName = info.getLabel(pm)
) )
} }

View File

@@ -1,83 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
public class OldModule extends BaseModule {
public static final Parcelable.Creator<OldModule> CREATOR = new Creator<OldModule>() {
/* It won't be used at any place */
@Override
public OldModule createFromParcel(Parcel source) {
return null;
}
@Override
public OldModule[] newArray(int size) {
return null;
}
};
private final SuFile mRemoveFile;
private final SuFile mDisableFile;
private final SuFile mUpdateFile;
private final boolean mUpdated;
private boolean mEnable;
private boolean mRemove;
public OldModule(String path) {
try {
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
} catch (NumberFormatException ignored) {
}
mRemoveFile = new SuFile(path, "remove");
mDisableFile = new SuFile(path, "disable");
mUpdateFile = new SuFile(path, "update");
if (getId().isEmpty()) {
int sep = path.lastIndexOf('/');
setId(path.substring(sep + 1));
}
if (getName().isEmpty()) {
setName(getId());
}
mEnable = !mDisableFile.exists();
mRemove = mRemoveFile.exists();
mUpdated = mUpdateFile.exists();
}
public void createDisableFile() {
mEnable = !mDisableFile.createNewFile();
}
public void removeDisableFile() {
mEnable = mDisableFile.delete();
}
public boolean isEnabled() {
return mEnable;
}
public void createRemoveFile() {
mRemove = mRemoveFile.createNewFile();
}
public void deleteRemoveFile() {
mRemove = !mRemoveFile.delete();
}
public boolean willBeRemoved() {
return mRemove;
}
public boolean isUpdated() {
return mUpdated;
}
}

View File

@@ -1,61 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.topjohnwu.magisk.utils.Utils;
import androidx.annotation.NonNull;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;
public static final int DENY = 1;
public static final int ALLOW = 2;
public int uid, policy = INTERACTIVE;
public long until;
public boolean logging = true, notification = true;
public String packageName, appName;
public ApplicationInfo info;
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
String[] pkgs = pm.getPackagesForUid(uid);
if (pkgs == null || pkgs.length == 0)
throw new PackageManager.NameNotFoundException();
this.uid = uid;
packageName = pkgs[0];
info = pm.getApplicationInfo(packageName, 0);
appName = Utils.getAppLabel(info, pm);
}
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
uid = values.getAsInteger("uid");
packageName = values.getAsString("package_name");
policy = values.getAsInteger("policy");
until = values.getAsInteger("until");
logging = values.getAsInteger("logging") != 0;
notification = values.getAsInteger("notification") != 0;
info = pm.getApplicationInfo(packageName, 0);
if (info.uid != uid)
throw new PackageManager.NameNotFoundException();
appName = info.loadLabel(pm).toString();
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("uid", uid);
values.put("package_name", packageName);
values.put("policy", policy);
values.put("until", until);
values.put("logging", logging ? 1 : 0);
values.put("notification", notification ? 1 : 0);
return values;
}
@Override
public int compareTo(@NonNull Policy policy) {
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
}
}

View File

@@ -1,114 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Utils;
import java.text.DateFormat;
import java.util.Date;
public class Repo extends BaseModule {
private Date mLastUpdate;
public Repo(String id) {
setId(id);
}
public Repo(Cursor c) {
super(c);
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
}
public Repo(Parcel p) {
super(p);
mLastUpdate = new Date(p.readLong());
}
public Repo(Repository repo) {
super(repo);
mLastUpdate = new Date(repo.getLastUpdate());
}
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
@Override
public Repo createFromParcel(Parcel source) {
return new Repo(source);
}
@Override
public Repo[] newArray(int size) {
return new Repo[size];
}
};
@Override
public void writeToParcel(Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeLong(mLastUpdate.getTime());
}
public void update() throws IllegalRepoException {
String[] props = Utils.dlString(getPropUrl()).split("\\n");
try {
parseProps(props);
} catch (NumberFormatException e) {
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
}
if (getVersionCode() < 0) {
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
}
}
public void update(Date lastUpdate) throws IllegalRepoException {
mLastUpdate = lastUpdate;
update();
}
@Override
public ContentValues getContentValues() {
ContentValues values = super.getContentValues();
values.put("last_update", mLastUpdate.getTime());
return values;
}
public String getZipUrl() {
return String.format(Const.Url.ZIP_URL, getId());
}
public String getPropUrl() {
return getFileUrl("module.prop");
}
public String getDetailUrl() {
return getFileUrl("README.md");
}
public String getFileUrl(String file) {
return String.format(Const.Url.FILE_URL, getId(), file);
}
public String getLastUpdateString() {
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
}
public Date getLastUpdate() {
return mLastUpdate;
}
public String getDownloadFilename() {
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
}
public class IllegalRepoException extends Exception {
IllegalRepoException(String message) {
super(message);
}
}
}

View File

@@ -1,56 +0,0 @@
package com.topjohnwu.magisk.model.entity;
import android.content.ContentValues;
import com.topjohnwu.magisk.utils.LocaleManager;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SuLogEntry {
public int fromUid, toUid, fromPid;
public String packageName, appName, command;
public boolean action;
public Date date;
public SuLogEntry(MagiskPolicy policy) {
fromUid = policy.getUid();
packageName = policy.getPackageName();
appName = policy.getAppName();
action = policy.getPolicy() == Policy.ALLOW;
}
public SuLogEntry(ContentValues values) {
fromUid = values.getAsInteger("from_uid");
packageName = values.getAsString("package_name");
appName = values.getAsString("app_name");
fromPid = values.getAsInteger("from_pid");
command = values.getAsString("command");
toUid = values.getAsInteger("to_uid");
action = values.getAsInteger("action") != 0;
date = new Date(values.getAsLong("time"));
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("from_uid", fromUid);
values.put("package_name", packageName);
values.put("app_name", appName);
values.put("from_pid", fromPid);
values.put("command", command);
values.put("to_uid", toUid);
values.put("action", action ? 1 : 0);
values.put("time", date.getTime());
return values;
}
public String getDateString() {
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.getLocale()).format(date);
}
public String getTimeString() {
return new SimpleDateFormat("h:mm a", LocaleManager.getLocale()).format(date);
}
}

View File

@@ -0,0 +1,35 @@
package com.topjohnwu.magisk.model.entity
import android.os.Parcelable
import kotlinx.android.parcel.Parcelize
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class UpdateInfo(
val app: ManagerJson = ManagerJson(),
val uninstaller: UninstallerJson = UninstallerJson(),
val magisk: MagiskJson = MagiskJson()
)
@JsonSerializable
data class UninstallerJson(
val link: String = ""
)
@JsonSerializable
data class MagiskJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = "",
val md5: String = ""
)
@Parcelize
@JsonSerializable
data class ManagerJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = ""
) : Parcelable

View File

@@ -1,3 +0,0 @@
package com.topjohnwu.magisk.model.entity
data class Version(val version: String, val versionCode: Int)

View File

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

View File

@@ -0,0 +1,106 @@
package com.topjohnwu.magisk.model.entity.internal
import android.content.Context
import android.os.Parcelable
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.extensions.cachedFile
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.model.entity.MagiskJson
import com.topjohnwu.magisk.model.entity.ManagerJson
import com.topjohnwu.magisk.model.entity.module.Repo
import kotlinx.android.parcel.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize
import java.io.File
sealed class DownloadSubject : Parcelable {
abstract val url: String
abstract val file: File
open val title: String get() = file.name
@Parcelize
data class Module(
val module: Repo,
val configuration: Configuration
) : DownloadSubject() {
override val url: String get() = module.zipUrl
@IgnoredOnParcel
override val file by lazy {
File(Config.downloadDirectory, module.downloadFilename)
}
}
@Parcelize
data class Manager(
val configuration: Configuration.APK
) : DownloadSubject() {
@IgnoredOnParcel
val manager: ManagerJson = Info.remote.app
override val title: String
get() = "MagiskManager-v${manager.version}(${manager.versionCode})"
override val url: String
get() = manager.link
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile("manager.apk")
}
}
sealed class Magisk : DownloadSubject() {
abstract val configuration: Configuration
val magisk: MagiskJson = Info.remote.magisk
@Parcelize
protected data class Flash(
override val configuration: Configuration
) : Magisk() {
override val url: String get() = magisk.link
override val title: String get() = "Magisk-v${magisk.version}(${magisk.versionCode})"
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile("magisk.zip")
}
}
@Parcelize
protected class Uninstall : Magisk() {
override val configuration: Configuration get() = Configuration.Uninstall
override val url: String get() = Info.remote.uninstaller.link
@IgnoredOnParcel
override val file by lazy {
get<Context>().cachedFile("uninstall.zip")
}
}
@Parcelize
protected class Download : Magisk() {
override val configuration: Configuration get() = Configuration.Download
override val url: String get() = magisk.link
@IgnoredOnParcel
override val file by lazy {
File(Config.downloadDirectory, "Magisk-v${magisk.version}(${magisk.versionCode}).zip")
}
}
companion object {
operator fun invoke(configuration: Configuration) = when (configuration) {
Configuration.Download -> Download()
Configuration.Uninstall -> Uninstall()
else -> Flash(configuration)
}
}
}
}

View File

@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.model.entity.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,71 @@
package com.topjohnwu.magisk.model.entity.module
import androidx.annotation.WorkerThread
import com.topjohnwu.magisk.Const
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
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 = ""
private val removeFile: SuFile = SuFile(path, "remove")
private val disableFile: SuFile = SuFile(path, "disable")
private val updateFile: SuFile = SuFile(path, "update")
val updated: Boolean = updateFile.exists()
var enable: Boolean = !disableFile.exists()
set(enable) {
field = if (enable) {
disableFile.delete()
} else {
!disableFile.createNewFile()
}
}
var remove: Boolean = removeFile.exists()
set(remove) {
field = if (remove) {
removeFile.createNewFile()
} else {
!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 {
@WorkerThread
fun loadModules(): List<Module> {
val moduleList = mutableListOf<Module>()
val path = SuFile(Const.MAGISK_PATH)
val modules =
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
for (file in modules) {
if (file.isFile) continue
val module = Module(Const.MAGISK_PATH + "/" + file.name)
moduleList.add(module)
}
return moduleList.sortedBy { it.name }
}
}
}

View File

@@ -0,0 +1,71 @@
package com.topjohnwu.magisk.model.entity.module
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.repository.StringRepository
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.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()
val readme get() = stringRepo.getReadme(this)
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
constructor(id: String) : this(id, "", "", "", -1, "", 0)
@Throws(IllegalRepoException::class)
fun update() {
val props = runCatching {
stringRepo.getMetadata(this).blockingGet()
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
}.getOrElse {
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
}
props.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)
fun update(lastUpdate: Date) {
last_update = lastUpdate.time
update()
}
class IllegalRepoException(message: String) : Exception(message)
companion object {
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)!!
}
}

View File

@@ -1,11 +1,26 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem import android.widget.TextView
import androidx.core.view.updateLayoutParams
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
class ConsoleRvItem(val item: String) : ComparableRvItem<ConsoleRvItem>() { class ConsoleRvItem(val item: String) : LenientRvItem<ConsoleRvItem>() {
override val layoutRes: Int = R.layout.item_console override val layoutRes: Int = R.layout.item_console
override fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {
val view = binding.root as TextView
view.measure(0, 0)
val desiredWidth = view.measuredWidth
view.updateLayoutParams { width = desiredWidth }
if (recyclerView.width < desiredWidth) {
recyclerView.requestLayout()
}
}
override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other) override fun contentSameAs(other: ConsoleRvItem) = itemSameAs(other)
override fun itemSameAs(other: ConsoleRvItem) = item == other.item override fun itemSameAs(other: ConsoleRvItem) = item == other.item
} }

View File

@@ -1,17 +1,17 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.state.IndeterminateState import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.inject import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.toggle import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) : class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() { ComparableRvItem<HideRvItem>() {

View File

@@ -0,0 +1,16 @@
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.databinding.ComparableRvItem
/**
* This item addresses issues where enclosing recycler has to be invalidated or generally
* manipulated with. This shouldn't be however necessary for 99.9% of use-cases. Refrain from using
* this item as it provides virtually no additional functionality. Stick with ComparableRvItem.
* */
abstract class LenientRvItem<in T> : ComparableRvItem<T>() {
open fun onBindingBound(binding: ViewDataBinding, recyclerView: RecyclerView) {}
}

View File

@@ -1,14 +1,14 @@
package com.topjohnwu.magisk.model.entity.recycler package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.timeFormatMedium
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskLog import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.timeFormatMedium import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.toTime import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.toggle
class LogRvItem : ComparableRvItem<LogRvItem>() { class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log override val layoutRes: Int = R.layout.item_page_log

View File

@@ -2,48 +2,58 @@ package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources import android.content.res.Resources
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.OldModule import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.model.entity.Repo import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.model.entity.Repository import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.utils.get import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.utils.toggle import com.topjohnwu.magisk.model.entity.module.Module
import com.topjohnwu.magisk.model.entity.module.Repo
import com.topjohnwu.magisk.utils.KObservableField
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() { class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
override val layoutRes: Int = R.layout.item_module override val layoutRes: Int = R.layout.item_module
val lastActionNotice = KObservableField("") val lastActionNotice = KObservableField("")
val isChecked = KObservableField(item.isEnabled) val isChecked = KObservableField(item.enable)
val isDeletable = KObservableField(item.willBeRemoved()) val isDeletable = KObservableField(item.remove)
init { init {
isChecked.addOnPropertyChangedCallback { isChecked.addOnPropertyChangedCallback {
when (it) { when (it) {
true -> item.removeDisableFile().notice(R.string.disable_file_removed) true -> {
false -> item.createDisableFile().notice(R.string.disable_file_created) item.enable = true
notice(R.string.disable_file_removed)
}
false -> {
item.enable = false
notice(R.string.disable_file_created)
}
} }
} }
isDeletable.addOnPropertyChangedCallback { isDeletable.addOnPropertyChangedCallback {
when (it) { when (it) {
true -> item.createRemoveFile().notice(R.string.remove_file_created) true -> {
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted) item.remove = true
notice(R.string.remove_file_created)
}
false -> {
item.remove = false
notice(R.string.remove_file_deleted)
}
} }
} }
when { when {
item.isUpdated -> notice(R.string.update_file_created) item.updated -> notice(R.string.update_file_created)
item.willBeRemoved() -> notice(R.string.remove_file_created) item.remove -> notice(R.string.remove_file_created)
} }
} }
fun toggle() = isChecked.toggle() fun toggle() = isChecked.toggle()
fun toggleDelete() = isDeletable.toggle() fun toggleDelete() = isDeletable.toggle()
@Suppress("unused") private fun notice(@StringRes info: Int) {
private fun Any.notice(@StringRes info: Int) {
lastActionNotice.value = get<Resources>().getString(info) lastActionNotice.value = get<Resources>().getString(info)
} }
@@ -57,15 +67,9 @@ class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() { class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
constructor(repo: Repository) : this(Repo(repo))
override val layoutRes: Int = R.layout.item_repo override val layoutRes: Int = R.layout.item_repo
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
&& item.lastUpdate == other.item.lastUpdate
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
&& item.detailUrl == other.item.detailUrl
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
} }

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