Compare commits

..

227 Commits

Author SHA1 Message Date
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
329 changed files with 6341 additions and 9288 deletions

2
.gitattributes vendored
View File

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

6
.gitmodules vendored
View File

@@ -19,3 +19,9 @@
[submodule "nanopb"]
path = native/jni/external/nanopb
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
**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

View File

@@ -17,8 +17,8 @@ android {
applicationId 'com.topjohnwu.magisk'
vectorDrawables.useSupportLibrary = true
multiDexEnabled true
versionName configProps['appVersion']
versionCode configProps['appVersionCode'] as Integer
versionName props['appVersion']
versionCode props['appVersionCode'] as Integer
}
buildTypes {
@@ -39,7 +39,7 @@ android {
exclude '/META-INF/*.kotlin_module'
exclude '/META-INF/rxkotlin.properties'
exclude '/androidsupportmultidexversion.txt'
exclude '/org/**'
exclude '/org/bouncycastle/**'
exclude '/kotlin/**'
exclude '/kotlinx/**'
}
@@ -55,7 +55,6 @@ androidExtensions {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':net')
implementation project(':shared')
implementation project(':signing')
@@ -63,13 +62,14 @@ dependencies {
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.github.pwittchen:reactivenetwork-rx2:3.0.6'
def vMarkwon = '3.0.1'
def vMarkwon = '3.1.0'
implementation "ru.noties.markwon:core:${vMarkwon}"
implementation "ru.noties.markwon:html:${vMarkwon}"
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
def vLibsu = '2.5.0'
def vLibsu = '2.5.1'
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
@@ -78,12 +78,13 @@ dependencies {
implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = "2.6.0"
def vRetrofit = '2.6.1'
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = "3.12.3"
def vOkHttp = '3.12.2'
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
@@ -101,14 +102,18 @@ dependencies {
}
def vRoom = "2.1.0"
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
kapt "androidx.room:room-compiler:${vRoom}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.work:work-runtime:2.0.1'
implementation 'androidx.transition:transition:1.2.0-alpha01'
implementation 'androidx.work:work-runtime:2.2.0'
implementation 'androidx.transition:transition:1.2.0-rc01'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha07'
implementation 'com.google.android.material:material:1.1.0-alpha10'
}

View File

@@ -17,9 +17,9 @@
#}
# Snet
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback {
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
void onResponse(int);
}
@@ -36,9 +36,6 @@
# Strip logging
-assumenosideeffects class timber.log.Timber.Tree { *; }
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
public *** debug(...);
}
# Excessive obfuscation
-repackageclasses 'a'

View File

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

View File

@@ -1,7 +1,7 @@
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 */
}

View File

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

View File

@@ -1,35 +1,42 @@
package com.topjohnwu.magisk
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.res.Configuration
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
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.extensions.get
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
import java.util.concurrent.ThreadPoolExecutor
open class App : Application(), Application.ActivityLifecycleCallbacks {
open class App : Application() {
lateinit var protectedContext: Context
@Volatile
private var foreground: Activity? = null
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)
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
RepoDatabase::class.java -> RepoDatabase_Impl()
else -> null
}
}
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
@@ -42,17 +49,7 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
modules(koinModules)
}
protectedContext = baseContext
self = this
deContext = base
if (Build.VERSION.SDK_INT >= 24) {
protectedContext = base.createDeviceProtectedStorageContext()
deContext = protectedContext
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
}
registerActivityLifecycleCallbacks(this)
registerActivityLifecycleCallbacks(get<ActivityTracker>())
Networking.init(base)
LocaleManager.setLocale(this)
@@ -62,67 +59,4 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
super.onConfigurationChanged(newConfig)
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
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
else -> null
}
}
}
@Deprecated("")
@JvmStatic
fun foreground(): Activity? {
val app: App by inject()
return app.foreground
}
}
}

View File

@@ -1,6 +1,6 @@
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.update.UpdateCheckService
import com.topjohnwu.magisk.ui.MainActivity
@@ -16,11 +16,10 @@ object ClassMap {
FlashActivity::class.java to a.f::class.java,
UpdateCheckService::class.java to a.g::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
)
@JvmStatic
operator fun <T : Class<*>>get(c: Class<*>): T {
return map.getOrElse(c) { throw IllegalArgumentException() } as T
}

View File

@@ -1,14 +1,20 @@
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.*
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
@@ -39,9 +45,9 @@ object Config : PreferenceModel, DBConfig {
const val CUSTOM_CHANNEL = "custom_channel"
const val LOCALE = "locale"
const val DARK_THEME = "dark_theme"
const val ETAG_KEY = "ETag"
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"
@@ -91,9 +97,10 @@ object Config : PreferenceModel, DBConfig {
}
private val defaultChannel =
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
else Value.DEFAULT_CHANNEL
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)
@@ -104,73 +111,26 @@ object Config : PreferenceModel, DBConfig {
var darkTheme by preference(Key.DARK_THEME, true)
var suReAuth by preference(Key.SU_REAUTH, false)
var checkUpdate by preference(Key.CHECK_UPDATES, true)
@JvmStatic
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, "")
@JvmStatic
var etagKey by preference(Key.ETAG_KEY, "")
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)
@JvmStatic
var suManager by dbStrings(Key.SU_MANAGER, "")
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 {
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")
val value: String = 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()
}
remove(Key.ETAG_KEY)
parsePrefs(this)
if (!prefs.contains(Key.UPDATE_CHANNEL))
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
@@ -184,12 +144,64 @@ object Config : PreferenceModel, DBConfig {
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
}
@JvmStatic
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().apply()
val xml = File("${get<Context>(Protected).filesDir.parent}/shared_prefs",
"${packageName}_preferences.xml")
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,39 +1,27 @@
package com.topjohnwu.magisk
import android.os.Environment
import android.os.Process
import java.io.File
object Const {
const val DEBUG_TAG = "MagiskManager"
// Paths
const val MAGISK_PATH = "/sbin/.magisk/img"
@JvmField
val EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)!!
@JvmField
var MAGISK_DISABLE_FILE = File("xxx")
const val TMP_FOLDER_PATH = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
// Versions
const val SNET_EXT_VER = 12
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
const val SNET_EXT_VER = 13
const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
// Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml"
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
@JvmField
val USER_ID = Process.myUid() / 100000
init {
EXTERNAL_PATH.mkdirs()
}
object MagiskVersion {
const val MIN_SUPPORT = 18000
}
@@ -53,35 +41,28 @@ object Const {
}
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 MODULE_INSTALLER =
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
const val PATREON_URL = "https://www.patreon.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 SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
@JvmField
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
private fun getRaw(where: String, name: String) =
"${GITHUB_RAW_API_URL}topjohnwu/magisk_files/$where/$name"
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
}
object Key {
// others
const val LINK_KEY = "Link"
const val IF_NONE_MATCH = "If-None-Match"
const val ETAG_KEY = "ETag"
// intents
const val OPEN_SECTION = "section"
const val INTENT_SET_NAME = "filename"
const val INTENT_SET_LINK = "link"
const val INTENT_SET_APP = "app_json"
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_REBOOT = "reboot"
}
@@ -94,5 +75,4 @@ object Const {
const val UNINSTALL = "uninstall"
}
}

View File

@@ -1,36 +0,0 @@
package com.topjohnwu.magisk;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
public final class Info {
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 void loadMagiskInfo() {
try {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
Config.setMagiskHide(Shell.su("magiskhide --status").exec().isSuccess());
} catch (NumberFormatException ignored) {
}
}
}

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

@@ -4,10 +4,11 @@ import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Const
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.toMap
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.utils.now
import timber.log.Timber
import java.util.concurrent.TimeUnit
@@ -47,12 +48,7 @@ class PolicyDao(
condition {
equals("uid", uid)
}
}.map { it.firstOrNull()?.toPolicy(context.packageManager) }
.doOnError {
if (it is PackageManager.NameNotFoundException) {
delete(uid).subscribe()
}
}
}.map { it.first().toPolicySafe() }
fun update(policy: MagiskPolicy) = query<Replace> {
values(policy.toMap())
@@ -62,8 +58,18 @@ class PolicyDao(
condition {
equals("uid/100000", Const.USER_ID)
}
}.flattenAsFlowable { it }
.map { it.toPolicy(context.packageManager) }
.toList()
}.map { it.mapNotNull { it.toPolicySafe() } }
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,109 +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 androidx.core.content.edit
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.entity.Repo
import java.util.*
@Deprecated("")
class RepoDatabaseHelper
constructor(context: Context) : SQLiteOpenHelper(context, "repo.db", null, DATABASE_VER) {
private val mDb: SQLiteDatabase = writableDatabase
val rawCursor: Cursor
@Deprecated("")
get() = mDb.query(TABLE_NAME, null, null, null, null, null, null)
val repoCursor: Cursor
@Deprecated("")
get() {
var orderBy: String? = null
when (Config.repoOrder) {
Config.Value.ORDER_NAME -> orderBy = "name COLLATE NOCASE"
Config.Value.ORDER_DATE -> orderBy = "last_update DESC"
}
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy)
}
val repoIDSet: Set<String>
@Deprecated("")
get() {
val set = HashSet<String>(300)
mDb.query(TABLE_NAME, null, null, null, null, null, null).use { c ->
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex("id")))
}
}
return set
}
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, DATABASE_VER)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
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.prefs.edit {
remove(Config.Key.ETAG_KEY)
}
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onUpgrade(db, 0, DATABASE_VER)
}
@Deprecated("")
fun clearRepo() {
mDb.delete(TABLE_NAME, null, null)
}
@Deprecated("")
fun removeRepo(id: String) {
mDb.delete(TABLE_NAME, "id=?", arrayOf(id))
}
@Deprecated("")
fun removeRepo(repo: Repo) {
removeRepo(repo.id)
}
@Deprecated("")
fun removeRepo(list: Iterable<String>) {
list.forEach {
mDb.delete(TABLE_NAME, "id=?", arrayOf(it))
}
}
@Deprecated("")
fun addRepo(repo: Repo) {
mDb.replace(TABLE_NAME, null, repo.contentValues)
}
@Deprecated("")
fun getRepo(id: String): Repo? {
mDb.query(TABLE_NAME, null, "id=?", arrayOf(id), null, null, null).use { c ->
if (c.moveToNext()) {
return Repo(c)
}
}
return null
}
companion object {
private val DATABASE_VER = 5
private val TABLE_NAME = "repos"
}
}

View File

@@ -1,35 +1,34 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.model.entity.MagiskConfig
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.http.GET
import retrofit2.http.Path
import retrofit2.http.Streaming
import retrofit2.http.Url
import retrofit2.adapter.rxjava2.Result
import retrofit2.http.*
interface GithubRawApiServices {
interface GithubRawServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchStableUpdate(): Single<MagiskConfig>
fun fetchStableUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<MagiskConfig>
fun fetchBetaUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/release.json")
fun fetchCanaryUpdate(): Single<MagiskConfig>
fun fetchCanaryUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
fun fetchCanaryDebugUpdate(): Single<MagiskConfig>
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
@GET
fun fetchCustomUpdate(@Url url: String): Single<MagiskConfig>
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
@GET("$MAGISK_FILES/{$REVISION}/snet.jar")
@Streaming
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
@@ -37,6 +36,13 @@ interface GithubRawApiServices {
@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
/**
@@ -47,6 +53,9 @@ interface GithubRawApiServices {
@Streaming
fun fetchFile(@Url url: String): Single<ResponseBody>
@GET
fun fetchString(@Url url: String): Single<String>
companion object {
private const val REVISION = "revision"
@@ -59,4 +68,14 @@ interface GithubRawApiServices {
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

@@ -2,7 +2,6 @@ package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.data.database.StringDao
import com.topjohnwu.magisk.utils.trimEmptyToNull
import io.reactivex.schedulers.Schedulers
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
@@ -23,8 +22,9 @@ interface DBConfig {
fun dbStrings(
name: String,
default: String
) = DBStringsValue(name, default)
default: String,
sync: Boolean = false
) = DBStringsValue(name, default, sync)
}
@@ -35,12 +35,10 @@ class DBSettingsValue(
private var value: Int? = null
private fun getKey(property: KProperty<*>) = name.trimEmptyToNull() ?: property.name
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
if (value == null)
value = thisRef.settingsDao.fetch(getKey(property), default).blockingGet()
value = thisRef.settingsDao.fetch(name, default).blockingGet()
return value!!
}
@@ -48,7 +46,7 @@ class DBSettingsValue(
synchronized(this) {
this.value = value
}
thisRef.settingsDao.put(getKey(property), value)
thisRef.settingsDao.put(name, value)
.subscribeOn(Schedulers.io())
.subscribe()
}
@@ -70,17 +68,16 @@ class DBBoolSettings(
class DBStringsValue(
private val name: String,
private val default: String
private val default: String,
private val sync: Boolean
) : ReadWriteProperty<DBConfig, String> {
private var value: String? = null
private fun getKey(property: KProperty<*>) = name.trimEmptyToNull() ?: property.name
@Synchronized
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
if (value == null)
value = thisRef.stringDao.fetch(getKey(property), default).blockingGet()
value = thisRef.stringDao.fetch(name, default).blockingGet()
return value!!
}
@@ -88,8 +85,22 @@ class DBStringsValue(
synchronized(this) {
this.value = value
}
thisRef.stringDao.put(getKey(property), value)
.subscribeOn(Schedulers.io())
.subscribe()
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

@@ -3,11 +3,10 @@ package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.LogDao
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.WrappedMagiskLog
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.superuser.Shell
import timber.log.Timber
import java.util.concurrent.TimeUnit
@@ -21,7 +20,6 @@ class LogRepository(
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
.filter { it.isNotEmpty() }
.map { Timber.i(it.toString()); it }
fun clearLogs() = logDao.deleteAll()
fun clearOutdated() = logDao.deleteOutdated()

View File

@@ -1,46 +1,25 @@
package com.topjohnwu.magisk.data.repository
import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices
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.HideTarget
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 io.reactivex.Single
class MagiskRepository(
private val context: Context,
private val apiRaw: GithubRawApiServices,
private val packageManager: PackageManager
private val apiRaw: GithubRawServices,
private val packageManager: PackageManager
) {
fun fetchMagisk() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.magisk.link) }
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
fun fetchManager() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.app.link) }
.map { it.writeToFile(context, FILE_MAGISK_APK) }
fun fetchUninstaller() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
fun fetchSafetynet() = apiRaw.fetchSafetynet()
fun fetchBootctl() = apiRaw
.fetchBootctl()
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
fun fetchUpdate() = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
@@ -50,34 +29,21 @@ class MagiskRepository(
else -> throw IllegalArgumentException()
}.flatMap {
// If remote version is lower than current installed, try switching to beta
if (it.magisk.versionCode.toIntOrNull() ?: -1 < Info.magiskVersionCode
if (it.magisk.versionCode < Info.magiskVersionCode
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
apiRaw.fetchBetaUpdate()
} else {
Single.just(it)
}
}.doOnSuccess {
Info.remoteMagiskVersionString = it.magisk.version
Info.remoteMagiskVersionCode = it.magisk.versionCode.toIntOrNull() ?: -1
Info.magiskLink = it.magisk.link
Info.magiskNoteLink = it.magisk.note
Info.magiskMD5 = it.magisk.hash
Info.remoteManagerVersionString = it.app.version
Info.remoteManagerVersionCode = it.app.versionCode.toIntOrNull() ?: -1
Info.managerLink = it.app.link
Info.managerNoteLink = it.app.note
Info.uninstallerLink = it.uninstaller.link
}
}.doOnSuccess { Info.remote = it }
fun fetchApps() =
Single.fromCallable { packageManager.getInstalledApplications(0) }
.flattenAsFlowable { it }
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map {
val label = Utils.getAppLabel(it, packageManager)
val label = it.getLabel(packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}
@@ -96,14 +62,8 @@ class MagiskRepository(
private val Boolean.state get() = if (this) "add" else "rm"
companion object {
const val FILE_MAGISK_ZIP = "magisk.zip"
const val FILE_MAGISK_APK = "magisk.apk"
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,
private val blacklist by lazy { listOf(
packageName,
"android",
"com.android.chrome",
"com.chrome.beta",
@@ -111,7 +71,7 @@ class MagiskRepository(
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
)
) }
}
}

View File

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

View File

@@ -1,18 +1,61 @@
package com.topjohnwu.magisk.di
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import androidx.preference.PreferenceManager
import com.skoumal.teanity.rxbus.RxBus
import com.topjohnwu.magisk.App
import org.koin.core.qualifier.named
import org.koin.dsl.module
val SUTimeout = named("su_timeout")
val Protected = named("protected")
val applicationModule = module {
single { RxBus() }
factory { get<Context>().resources }
factory { get<Context>() as App }
factory { get<Context>().packageManager }
factory(Protected) { get<App>().protectedContext }
factory(Protected) { createDEContext(get()) }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
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
import android.content.Context
import androidx.room.Room
import com.topjohnwu.magisk.data.database.*
import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.tasks.RepoUpdater
import org.koin.dsl.module
@@ -10,6 +12,12 @@ val databaseModule = module {
single { PolicyDao(get()) }
single { SettingsDao() }
single { StringDao() }
single { RepoDatabaseHelper(get()) }
single { UpdateRepos(get()) }
single { createRepoDatabase(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,
databaseModule,
repositoryModule,
viewModelModules,
miscModule
viewModelModules
)

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

@@ -4,23 +4,23 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
import retrofit2.CallAdapter
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.moshi.MoshiConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import se.ansman.kotshi.KotshiJsonAdapterFactory
val networkingModule = module {
single { createOkHttpClient() }
single { createConverterFactory() }
single { createCallAdapterFactory() }
single { createRetrofit(get(), get(), get()) }
single { createApiService<GithubRawApiServices>(get(), Const.Url.GITHUB_RAW_API_URL) }
single { createMoshiConverterFactory() }
single { createRetrofit(get(), get()) }
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
}
fun createOkHttpClient(): OkHttpClient {
@@ -28,7 +28,7 @@ fun createOkHttpClient(): OkHttpClient {
if (BuildConfig.DEBUG) {
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
level = HttpLoggingInterceptor.Level.HEADERS
}
builder.addInterceptor(httpLoggingInterceptor)
}
@@ -36,34 +36,26 @@ fun createOkHttpClient(): OkHttpClient {
return builder.build()
}
fun createConverterFactory(): Converter.Factory {
fun createMoshiConverterFactory(): MoshiConverterFactory {
val moshi = Moshi.Builder()
.add(JsonAdapterFactory.INSTANCE)
.add(KotshiJsonAdapterFactory)
.build()
return MoshiConverterFactory.create(moshi)
}
fun createCallAdapterFactory(): CallAdapter.Factory {
return RxJava2CallAdapterFactory.create()
}
fun createRetrofit(
okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
callAdapterFactory: CallAdapter.Factory
converterFactory: MoshiConverterFactory
): Retrofit.Builder {
return Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(converterFactory)
.addCallAdapterFactory(callAdapterFactory)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
}
@KotshiJsonAdapterFactory
abstract class JsonAdapterFactory : JsonAdapter.Factory {
companion object {
val INSTANCE: JsonAdapterFactory = KotshiJsonAdapterFactory
}
}
abstract class JsonAdapterFactory : JsonAdapter.Factory
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
return retrofitBuilder

View File

@@ -1,13 +1,13 @@
package com.topjohnwu.magisk.di
import com.topjohnwu.magisk.data.repository.AppRepository
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
val repositoryModule = module {
single { MagiskRepository(get(), get(), get()) }
single { MagiskRepository(get(), get()) }
single { LogRepository(get()) }
single { AppRepository(get()) }
single { StringRepository(get()) }
}

View File

@@ -20,6 +20,8 @@ val viewModelModules = module {
viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), 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()) }
}

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.extensions
import android.content.Context
import android.content.Intent
@@ -7,17 +7,16 @@ 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.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.utils.FileProvider
import com.topjohnwu.magisk.utils.currentLocale
import java.io.File
import java.io.FileNotFoundException
val packageName: String
get() {
val app: App by inject()
return app.packageName
}
val packageName: String get() = get<Context>().packageName
val PackageInfo.processes
get() = activities?.processNames.orEmpty() +
@@ -50,21 +49,22 @@ val ApplicationInfo.packageInfo: PackageInfo?
}
}
val Uri.fileName: String get() {
var name: String? = null
App.self.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)
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()
}
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)
@@ -80,24 +80,42 @@ fun PackageManager.providers(packageName: String) =
fun Context.rawResource(id: Int) = resources.openRawResource(id)
fun Context.readUri(uri: Uri) = contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
fun ApplicationInfo.findAppLabel(pm: PackageManager): String {
return pm.getApplicationLabel(this)?.toString().orEmpty()
}
fun Context.readUri(uri: Uri) =
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun File.provide(): Uri {
val context: Context by inject()
return FileProvider.getUriForFile(context, "com.topjohnwu.magisk.fileprovider", this)
fun File.provide(context: Context = get()): Uri {
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
}
fun File.mv(destination: File) {
inputStream().copyTo(destination)
inputStream().writeTo(destination)
deleteRecursively()
}
fun String.toFile() = File(this)
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
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()
}

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.extensions
import com.skoumal.teanity.util.KObservableField

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.parameter.ParametersDefinition
@@ -6,12 +6,12 @@ import org.koin.core.qualifier.Qualifier
fun getKoin() = GlobalContext.get().koin
inline fun <reified T : Any> inject(
inline fun <reified T> inject(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
) = lazy { get<T>(qualifier, parameters) }
inline fun <reified T : Any> get(
inline fun <reified T> get(
qualifier: Qualifier? = null,
noinline parameters: ParametersDefinition? = null
): T = getKoin().get(qualifier, parameters)

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.extensions
import androidx.collection.SparseArrayCompat
import androidx.databinding.ObservableList

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.extensions
import io.reactivex.Single
import io.reactivex.functions.BiFunction

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.extensions
import com.topjohnwu.magisk.Info
import com.topjohnwu.superuser.Shell

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.utils
package com.topjohnwu.magisk.extensions
import android.content.res.Resources
@@ -20,4 +20,8 @@ fun Int.res(vararg args: Any): String {
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.ParseException
import java.text.SimpleDateFormat
@@ -13,8 +14,7 @@ fun String.toTime(format: DateFormat) = try {
-1L
}
private val locale get() = LocaleManager.locale
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'", locale) }
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale) }
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", LocaleManager.locale) }
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }

View File

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

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 androidx.annotation.Nullable;
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;
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,142 @@
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.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 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 -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!))
.addAction(0, R.string.download_open_self, fileIntent(subject.file))
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 -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!))
.addAction(0, R.string.download_open_self, fileIntent(subject.file))
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)
.chooser()
}
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,86 @@
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 java.util.*
import kotlin.random.Random.Default.nextInt
abstract class NotificationService : Service() {
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.skoumal.teanity.extensions.subscribeK
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.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 androidx.annotation.NonNull;
import java.util.List;
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.graphics.drawable.Drawable
import com.topjohnwu.magisk.utils.packageInfo
import com.topjohnwu.magisk.utils.processes
import com.topjohnwu.magisk.extensions.packageInfo
import com.topjohnwu.magisk.extensions.processes
class HideAppInfo(
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
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.utils.timeFormatTime
import com.topjohnwu.magisk.utils.toTime
import java.util.*
data class MagiskLog(

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.Const
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 = "${Const.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.PackageManager
import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
@@ -50,7 +51,7 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
logging = get("logging")?.toIntOrNull() != 0,
notification = get("notification")?.toIntOrNull() != 0,
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 androidx.annotation.NonNull;
import com.topjohnwu.magisk.utils.Utils;
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.INSTANCE.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.INSTANCE.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.INSTANCE.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

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

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

@@ -6,12 +6,12 @@ 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.extensions.inject
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toggle
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
ComparableRvItem<HideRvItem>() {

View File

@@ -4,11 +4,11 @@ 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.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.WrappedMagiskLog
import com.topjohnwu.magisk.utils.timeFormatMedium
import com.topjohnwu.magisk.utils.toTime
import com.topjohnwu.magisk.utils.toggle
class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log

View File

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

View File

@@ -6,19 +6,18 @@ import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toggle
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
override val layoutRes: Int = R.layout.item_policy
val isExpanded = KObservableField(false)
val isEnabled = KObservableField(item.policy == Policy.ALLOW)
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
val shouldNotify = KObservableField(item.notification)
val shouldLog = KObservableField(item.logging)
@@ -28,7 +27,7 @@ class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvIte
private val currentStateItem
get() = item.copy(
policy = if (isEnabled.value) Policy.ALLOW else Policy.DENY,
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
notification = shouldNotify.value,
logging = shouldLog.value
)

View File

@@ -2,8 +2,7 @@ package com.topjohnwu.magisk.model.events
import android.app.Activity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.module.Repo
import io.reactivex.subjects.PublishSubject
@@ -36,5 +35,4 @@ class PermissionEvent(
class BackPressEvent : ViewEvent()
class SuDialogEvent(val policy: Policy) : ViewEvent()
class DieEvent : ViewEvent()

View File

@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.model.flash
import android.content.Context
import android.net.Uri
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.tasks.FlashZip
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler

View File

@@ -5,10 +5,11 @@ import com.topjohnwu.magisk.tasks.MagiskInstaller
import com.topjohnwu.superuser.Shell
sealed class Patching(
file: Uri,
private val console: MutableList<String>,
logs: MutableList<String>,
private val resultListener: FlashResultListener
) : MagiskInstaller(console, logs) {
) : MagiskInstaller(file, console, logs) {
override fun onResult(success: Boolean) {
if (success) {
@@ -21,29 +22,32 @@ sealed class Patching(
}
class File(
file: Uri,
private val uri: Uri,
console: MutableList<String>,
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
) : Patching(file, console, logs, resultListener) {
override fun operations() =
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
}
class SecondSlot(
file: Uri,
console: MutableList<String>,
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
) : Patching(file, console, logs, resultListener) {
override fun operations() =
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
}
class Direct(
file: Uri,
console: MutableList<String>,
logs: MutableList<String>,
resultListener: FlashResultListener
) : Patching(console, logs, resultListener) {
) : Patching(file, console, logs, resultListener) {
override fun operations() =
findImage() && extractZip() && patchBoot() && flashBoot()
}

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import com.topjohnwu.magisk.extensions.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import com.topjohnwu.magisk.extensions.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import com.topjohnwu.magisk.extensions.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import com.topjohnwu.magisk.extensions.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import com.topjohnwu.magisk.extensions.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import com.topjohnwu.magisk.extensions.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

View File

@@ -7,20 +7,23 @@ import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.reboot
import com.topjohnwu.magisk.model.download.DownloadService
import com.topjohnwu.magisk.model.entity.ManagerJson
import com.topjohnwu.magisk.model.entity.internal.Configuration
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.DownloadApp
import com.topjohnwu.magisk.utils.SuLogger
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.reboot
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
open class GeneralReceiver : BroadcastReceiver() {
private val appRepo: AppRepository by inject()
private val policyDB: PolicyDao by inject()
companion object {
const val REQUEST = "request"
@@ -30,21 +33,19 @@ open class GeneralReceiver : BroadcastReceiver() {
}
private fun getPkg(intent: Intent): String {
return intent.data?.encodedSchemeSpecificPart ?: ""
return intent.data?.encodedSchemeSpecificPart.orEmpty()
}
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null)
return
var action: String? = intent.action ?: return
when (action) {
intent ?: return
when (intent.action ?: return) {
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
action = intent.getStringExtra("action")
val action = intent.getStringExtra("action")
if (action == null) {
// Actual boot completed event
Shell.su("mm_patch_dtbo").submit { result ->
if (result.isSuccess)
Notifications.dtboPatched()
Shell.su("mm_patch_dtbo").submit {
if (it.isSuccess)
Notifications.dtboPatched(context)
}
return
}
@@ -65,16 +66,20 @@ open class GeneralReceiver : BroadcastReceiver() {
Intent.ACTION_PACKAGE_REPLACED ->
// This will only work pre-O
if (Config.suReAuth)
appRepo.delete(getPkg(intent)).blockingGet()
policyDB.delete(getPkg(intent)).blockingGet()
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
appRepo.delete(pkg).blockingGet()
policyDB.delete(pkg).blockingGet()
"magiskhide --rm $pkg".su().blockingGet()
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
Const.Key.BROADCAST_MANAGER_UPDATE -> {
Info.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK)
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME))
intent.getParcelableExtra<ManagerJson>(Const.Key.INTENT_SET_APP)?.let {
Info.remote = Info.remote.copy(app = it)
}
DownloadService(context) {
subject = DownloadSubject.Manager(Configuration.APK.Upgrade)
}
}
Const.Key.BROADCAST_REBOOT -> reboot()
}

View File

@@ -4,8 +4,8 @@ import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.model.worker.DelegateWorker
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell
@@ -18,10 +18,10 @@ class UpdateCheckService : DelegateWorker() {
Shell.getShell()
return runCatching {
magiskRepo.fetchUpdate().blockingGet()
if (BuildConfig.VERSION_CODE < Info.remoteManagerVersionCode)
Notifications.managerUpdate()
else if (Info.magiskVersionCode < Info.remoteMagiskVersionCode)
Notifications.magiskUpdate()
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
Notifications.managerUpdate(applicationContext)
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
Notifications.magiskUpdate(applicationContext)
ListenableWorker.Result.success()
}.getOrElse {
ListenableWorker.Result.failure()

View File

@@ -1,84 +0,0 @@
package com.topjohnwu.magisk.model.worker;
import android.content.Context;
import android.net.Network;
import android.net.Uri;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public abstract class DelegateWorker {
private ListenableWorker worker;
@NonNull
public abstract ListenableWorker.Result doWork();
public void onStopped() {}
public void setActualWorker(ListenableWorker w) {
worker = w;
}
@NonNull
public Context getApplicationContext() {
return worker.getApplicationContext();
}
@NonNull
public UUID getId() {
return worker.getId();
}
@NonNull
public Data getInputData() {
return worker.getInputData();
}
@NonNull
public Set<String> getTags() {
return worker.getTags();
}
@NonNull
@RequiresApi(24)
public List<Uri> getTriggeredContentUris() {
return worker.getTriggeredContentUris();
}
@NonNull
@RequiresApi(24)
public List<String> getTriggeredContentAuthorities() {
return worker.getTriggeredContentAuthorities();
}
@Nullable
@RequiresApi(28)
public Network getNetwork() {
return worker.getNetwork();
}
public int getRunAttemptCount() {
return worker.getRunAttemptCount();
}
@NonNull
@MainThread
public ListenableFuture<ListenableWorker.Result> startWork() {
return worker.startWork();
}
public boolean isStopped() {
return worker.isStopped();
}
}

View File

@@ -0,0 +1,59 @@
package com.topjohnwu.magisk.model.worker
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,7 +1,7 @@
package com.topjohnwu.magisk.model.zip
import com.topjohnwu.magisk.utils.forEach
import com.topjohnwu.magisk.utils.withStreams
import com.topjohnwu.magisk.extensions.forEach
import com.topjohnwu.magisk.extensions.withStreams
import com.topjohnwu.superuser.io.SuFile
import java.io.File
import java.util.zip.ZipInputStream

View File

@@ -1,12 +1,12 @@
package com.topjohnwu.magisk.tasks
import android.content.Context
import android.net.Uri
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.utils.fileName
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.readUri
import com.topjohnwu.magisk.extensions.fileName
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.readUri
import com.topjohnwu.magisk.utils.unzip
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
@@ -20,8 +20,11 @@ abstract class FlashZip(
private val logs: MutableList<String>
) {
private val app: App by inject()
private val tmpFile: File = File(app.cacheDir, "install.zip")
private val context: Context by inject()
private val installFolder = File(context.cacheDir, "flash").apply {
if (!exists()) mkdirs()
}
private val tmpFile: File = File(installFolder, "install.zip")
@Throws(IOException::class)
private fun unzipAndCheck(): Boolean {
@@ -40,7 +43,7 @@ abstract class FlashZip(
console.add("- Copying zip to temp directory")
runCatching {
app.readUri(mUri).use { input ->
context.readUri(mUri).use { input ->
tmpFile.outputStream().use { out -> input.copyTo(out) }
}
}.getOrElse {

View File

@@ -1,380 +0,0 @@
package com.topjohnwu.magisk.tasks;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.Info;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.DownloadProgressListener;
import com.topjohnwu.net.Networking;
import com.topjohnwu.signing.SignBoot;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.internal.NOPList;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import com.topjohnwu.superuser.io.SuFileOutputStream;
import org.kamranzafar.jtar.TarEntry;
import org.kamranzafar.jtar.TarHeader;
import org.kamranzafar.jtar.TarInputStream;
import org.kamranzafar.jtar.TarOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
public abstract class MagiskInstaller {
protected String srcBoot;
protected File destFile;
protected File installDir;
private List<String> console, logs;
private boolean isTar = false;
private class ProgressLog implements DownloadProgressListener {
private int prev = -1;
private int location;
@Override
public void onProgress(long bytesDownloaded, long totalBytes) {
if (prev < 0) {
location = console.size();
console.add("... 0%");
}
int curr = (int) (100 * bytesDownloaded / totalBytes);
if (prev != curr) {
prev = curr;
console.set(location, "... " + prev + "%");
}
}
}
protected MagiskInstaller() {
console = NOPList.getInstance();
logs = NOPList.getInstance();
}
public MagiskInstaller(List<String> out, List<String> err) {
console = out;
logs = err;
installDir = new File(App.deContext.getFilesDir().getParent(), "install");
Shell.sh("rm -rf " + installDir).exec();
installDir.mkdirs();
}
protected boolean findImage() {
srcBoot = ShellUtils.fastCmd("find_boot_image", "echo \"$BOOTIMAGE\"");
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image");
return false;
}
console.add("- Target image: " + srcBoot);
return true;
}
protected boolean findSecondaryImage() {
String slot = ShellUtils.fastCmd("echo $SLOT");
String target = (TextUtils.equals(slot, "_a") ? "_b" : "_a");
console.add("- Target slot: " + target);
srcBoot = ShellUtils.fastCmd(
"SLOT=" + target,
"find_boot_image",
"SLOT=" + slot,
"echo \"$BOOTIMAGE\""
);
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image");
return false;
}
console.add("- Target image: " + srcBoot);
return true;
}
protected boolean extractZip() {
String arch;
if (Build.VERSION.SDK_INT >= 21) {
List<String> abis = Arrays.asList(Build.SUPPORTED_ABIS);
arch = abis.contains("x86") ? "x86" : "arm";
} else {
arch = TextUtils.equals(Build.CPU_ABI, "x86") ? "x86" : "arm";
}
console.add("- Device platform: " + Build.CPU_ABI);
File zip = new File(App.self.getCacheDir(), "magisk.zip");
if (!ShellUtils.checkSum("MD5", zip, Info.magiskMD5)) {
console.add("- Downloading zip");
Networking.get(Info.magiskLink)
.setDownloadProgressListener(new ProgressLog())
.execForFile(zip);
} else {
console.add("- Existing zip found");
}
try {
ZipInputStream zi = new ZipInputStream(new BufferedInputStream(
new FileInputStream(zip), (int) zip.length()));
ZipEntry ze;
while ((ze = zi.getNextEntry()) != null) {
if (ze.isDirectory())
continue;
String name = null;
String[] names = { arch + "/", "common/", "META-INF/com/google/android/update-binary" };
for (String n : names) {
if (ze.getName().startsWith(n)) {
name = ze.getName().substring(ze.getName().lastIndexOf('/') + 1);
break;
}
}
if (name == null && ze.getName().startsWith("chromeos/"))
name = ze.getName();
if (name == null)
continue;
File dest = (installDir instanceof SuFile) ?
new SuFile(installDir, name) :
new File(installDir, name);
dest.getParentFile().mkdirs();
try (OutputStream out = new SuFileOutputStream(dest)) {
ShellUtils.pump(zi, out);
}
}
} catch (IOException e) {
console.add("! Cannot unzip zip");
return false;
}
File init64 = SuFile.open(installDir, "magiskinit64");
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.length != 0) {
init64.renameTo(SuFile.open(installDir, "magiskinit"));
} else {
init64.delete();
}
Shell.sh("cd " + installDir, "chmod 755 *").exec();
return true;
}
private TarEntry newEntry(String name, long size) {
console.add("-- Writing: " + name);
return new TarEntry(TarHeader.createHeader(name, size, 0, false, 0644));
}
private void handleTar(InputStream in) throws IOException {
console.add("- Processing tar file");
boolean vbmeta = false;
try (TarInputStream tarIn = new TarInputStream(in);
TarOutputStream tarOut = new TarOutputStream(destFile)) {
TarEntry entry;
while ((entry = tarIn.getNextEntry()) != null) {
if (entry.getName().contains("boot.img")
|| entry.getName().contains("recovery.img")) {
String name = entry.getName();
console.add("-- Extracting: " + name);
File extract = new File(installDir, name);
try (FileOutputStream fout = new FileOutputStream(extract)) {
ShellUtils.pump(tarIn, fout);
}
if (name.contains(".lz4")) {
console.add("-- Decompressing: " + name);
Shell.sh("./magiskboot --decompress " + extract).to(console).exec();
}
} else if (entry.getName().contains("vbmeta.img")) {
vbmeta = true;
ByteBuffer buf = ByteBuffer.allocate(256);
buf.put("AVB0".getBytes()); // magic
buf.putInt(1); // required_libavb_version_major
buf.putInt(120, 2); // flags
buf.position(128); // release_string
buf.put("avbtool 1.1.0".getBytes());
tarOut.putNextEntry(newEntry("vbmeta.img", 256));
tarOut.write(buf.array());
} else {
console.add("-- Writing: " + entry.getName());
tarOut.putNextEntry(entry);
ShellUtils.pump(tarIn, tarOut);
}
}
File boot = SuFile.open(installDir, "boot.img");
File recovery = SuFile.open(installDir, "recovery.img");
if (vbmeta && recovery.exists() && boot.exists()) {
// Install Magisk to recovery
srcBoot = recovery.getPath();
// Repack boot image to prevent restore
Shell.sh(
"./magiskboot --unpack boot.img",
"./magiskboot --repack boot.img",
"./magiskboot --cleanup",
"mv new-boot.img boot.img").exec();
try (InputStream sin = new SuFileInputStream(boot)) {
tarOut.putNextEntry(newEntry("boot.img", boot.length()));
ShellUtils.pump(sin, tarOut);
}
boot.delete();
} else {
if (!boot.exists()) {
console.add("! No boot image found");
throw new IOException();
}
srcBoot = boot.getPath();
}
}
}
protected boolean handleFile(Uri uri) {
try (InputStream in = new BufferedInputStream(App.self.getContentResolver().openInputStream(uri))) {
in.mark(500);
byte[] magic = new byte[5];
if (in.skip(257) != 257 || in.read(magic) != magic.length) {
console.add("! Invalid file");
return false;
}
in.reset();
if (Arrays.equals(magic, "ustar".getBytes())) {
isTar = true;
destFile = new File(Const.EXTERNAL_PATH, "magisk_patched.tar");
handleTar(in);
} else {
// Raw image
srcBoot = new File(installDir, "boot.img").getPath();
destFile = new File(Const.EXTERNAL_PATH, "magisk_patched.img");
console.add("- Copying image to cache");
try (OutputStream out = new FileOutputStream(srcBoot)) {
ShellUtils.pump(in, out);
}
}
} catch (IOException e) {
console.add("! Process error");
e.printStackTrace();
return false;
}
return true;
}
protected boolean patchBoot() {
boolean isSigned;
try (InputStream in = new SuFileInputStream(srcBoot)) {
isSigned = SignBoot.verifySignature(in, null);
if (isSigned) {
console.add("- Boot image is signed with AVB 1.0");
}
} catch (IOException e) {
console.add("! Unable to check signature");
return false;
}
if (!Shell.sh(Utils.INSTANCE.fmt(
"KEEPFORCEENCRYPT=%b KEEPVERITY=%b RECOVERYMODE=%b " +
"sh update-binary sh boot_patch.sh %s",
Info.keepEnc, Info.keepVerity, Info.recovery, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
Shell.Job job = Shell.sh("./magiskboot --cleanup",
"mv bin/busybox busybox",
"rm -rf magisk.apk bin boot.img update-binary",
"cd /");
File patched = new File(installDir, "new-boot.img");
if (isSigned) {
console.add("- Signing boot image with test keys");
File signed = new File(installDir, "signed.img");
try (InputStream in = new SuFileInputStream(patched);
OutputStream out = new BufferedOutputStream(new FileOutputStream(signed))) {
SignBoot.doSignature("/boot", in, out, null, null);
} catch (IOException e) {
return false;
}
job.add("mv -f " + signed + " " + patched);
}
job.exec();
return true;
}
protected boolean flashBoot() {
if (!Shell.su(Utils.INSTANCE.fmt("direct_install %s %s", installDir, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
if (!Info.keepVerity)
Shell.su("patch_dtbo_image").to(console, logs).exec();
return true;
}
protected boolean storeBoot() {
File patched = SuFile.open(installDir, "new-boot.img");
try {
OutputStream os;
if (isTar) {
os = new TarOutputStream(destFile, true);
((TarOutputStream) os).putNextEntry(newEntry(
srcBoot.contains("recovery") ? "recovery.img" : "boot.img",
patched.length()));
} else {
os = new BufferedOutputStream(new FileOutputStream(destFile));
}
try (InputStream in = new SuFileInputStream(patched);
OutputStream out = os) {
ShellUtils.pump(in, out);
}
} catch (IOException e) {
console.add("! Failed to output to " + destFile);
e.printStackTrace();
}
patched.delete();
console.add("");
console.add("****************************");
console.add(" Output file is placed in ");
console.add(" " + destFile + " ");
console.add("****************************");
return true;
}
protected boolean postOTA() {
SuFile bootctl = new SuFile("/data/adb/bootctl");
try (InputStream in = Networking.get(Const.Url.BOOTCTL_URL).execForInputStream().getResult();
OutputStream out = new SuFileOutputStream(bootctl)) {
ShellUtils.pump(in, out);
} catch (IOException e) {
e.printStackTrace();
return false;
}
Shell.su("post_ota " + bootctl.getParent()).exec();
console.add("***************************************");
console.add(" Next reboot will boot to second slot!");
console.add("***************************************");
return true;
}
@WorkerThread
protected abstract boolean operations();
@MainThread
protected abstract void onResult(boolean success);
public void exec() {
App.THREAD_POOL.execute(() -> {
boolean b = operations();
UiThreadHandler.run(() -> onResult(b));
});
}
}

View File

@@ -0,0 +1,356 @@
package com.topjohnwu.magisk.tasks
import android.content.Context
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.signing.SignBoot
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import com.topjohnwu.superuser.internal.NOPList
import com.topjohnwu.superuser.io.SuFile
import com.topjohnwu.superuser.io.SuFileInputStream
import com.topjohnwu.superuser.io.SuFileOutputStream
import io.reactivex.Single
import org.kamranzafar.jtar.TarEntry
import org.kamranzafar.jtar.TarHeader
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import timber.log.Timber
import java.io.*
import java.nio.ByteBuffer
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
abstract class MagiskInstaller {
protected lateinit var srcBoot: String
protected lateinit var destFile: File
protected lateinit var installDir: File
protected lateinit var zipUri: Uri
private val console: MutableList<String>
private val logs: MutableList<String>
private var isTar = false
private val service: GithubRawServices by inject()
private val context: Context by inject()
protected constructor() {
console = NOPList.getInstance()
logs = NOPList.getInstance()
}
constructor(zip: Uri, out: MutableList<String>, err: MutableList<String>) {
console = out
logs = err
zipUri = zip
installDir = File(get<Context>(Protected).filesDir.parent, "install")
"rm -rf $installDir".sh()
installDir.mkdirs()
}
protected fun findImage(): Boolean {
srcBoot = "find_boot_image; echo \"\$BOOTIMAGE\"".fsh()
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image")
return false
}
console.add("- Target image: $srcBoot")
return true
}
protected fun findSecondaryImage(): Boolean {
val slot = "echo \$SLOT".fsh()
val target = if (slot == "_a") "_b" else "_a"
console.add("- Target slot: $target")
srcBoot = arrayOf(
"SLOT=$target",
"find_boot_image",
"SLOT=$slot",
"echo \"\$BOOTIMAGE\"").fsh()
if (srcBoot.isEmpty()) {
console.add("! Unable to detect target image")
return false
}
console.add("- Target image: $srcBoot")
return true
}
protected fun extractZip(): Boolean {
val arch: String
arch = if (Build.VERSION.SDK_INT >= 21) {
val abis = listOf(*Build.SUPPORTED_ABIS)
if (abis.contains("x86")) "x86" else "arm"
} else {
if (TextUtils.equals(Build.CPU_ABI, "x86")) "x86" else "arm"
}
console.add("- Device platform: " + Build.CPU_ABI)
try {
ZipInputStream(context.readUri(zipUri).buffered()).use { zi ->
lateinit var ze: ZipEntry
while (zi.nextEntry?.let { ze = it } != null) {
if (ze.isDirectory)
continue
var name: String? = null
val names = arrayOf("$arch/", "common/", "META-INF/com/google/android/update-binary")
for (n in names) {
ze.name.run {
if (startsWith(n)) {
name = substring(lastIndexOf('/') + 1)
}
}
name ?: continue
break
}
if (name == null && ze.name.startsWith("chromeos/"))
name = ze.name
if (name == null)
continue
val dest = if (installDir is SuFile)
SuFile(installDir, name)
else
File(installDir, name)
dest.parentFile!!.mkdirs()
SuFileOutputStream(dest).use { zi.copyTo(it) }
}
}
} catch (e: IOException) {
console.add("! Cannot unzip zip")
Timber.e(e)
return false
}
val init64 = SuFile.open(installDir, "magiskinit64")
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.isNotEmpty()) {
init64.renameTo(SuFile.open(installDir, "magiskinit"))
} else {
init64.delete()
}
"cd $installDir; chmod 755 *".sh()
return true
}
private fun newEntry(name: String, size: Long): TarEntry {
console.add("-- Writing: $name")
return TarEntry(TarHeader.createHeader(name, size, 0, false, 420 /* 0644 */))
}
@Throws(IOException::class)
private fun handleTar(input: InputStream) {
console.add("- Processing tar file")
var vbmeta = false
withStreams(TarInputStream(input), TarOutputStream(destFile)) { tarIn, tarOut ->
lateinit var entry: TarEntry
while (tarIn.nextEntry?.let { entry = it } != null) {
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
val name = entry.name
console.add("-- Extracting: $name")
val extract = File(installDir, name)
FileOutputStream(extract).use { tarIn.copyTo(it) }
if (name.contains(".lz4")) {
console.add("-- Decompressing: $name")
"./magiskboot --decompress $extract".sh()
}
} else if (entry.name.contains("vbmeta.img")) {
vbmeta = true
val buf = ByteBuffer.allocate(256)
buf.put("AVB0".toByteArray()) // magic
buf.putInt(1) // required_libavb_version_major
buf.putInt(120, 2) // flags
buf.position(128) // release_string
buf.put("avbtool 1.1.0".toByteArray())
tarOut.putNextEntry(newEntry("vbmeta.img", 256))
tarOut.write(buf.array())
} else {
console.add("-- Writing: " + entry.name)
tarOut.putNextEntry(entry)
tarIn.copyTo(tarOut)
}
}
val boot = SuFile.open(installDir, "boot.img")
val recovery = SuFile.open(installDir, "recovery.img")
if (vbmeta && recovery.exists() && boot.exists()) {
// Install Magisk to recovery
srcBoot = recovery.path
// Repack boot image to prevent restore
arrayOf(
"./magiskboot --unpack boot.img",
"./magiskboot --repack boot.img",
"./magiskboot --cleanup",
"mv new-boot.img boot.img").sh()
SuFileInputStream(boot).use {
tarOut.putNextEntry(newEntry("boot.img", boot.length()))
it.copyTo(tarOut)
}
boot.delete()
} else {
if (!boot.exists()) {
console.add("! No boot image found")
throw IOException()
}
srcBoot = boot.path
}
}
}
protected fun handleFile(uri: Uri): Boolean {
try {
context.readUri(uri).buffered().use {
it.mark(500)
val magic = ByteArray(5)
if (it.skip(257) != 257L || it.read(magic) != magic.size) {
console.add("! Invalid file")
return false
}
it.reset()
if (Arrays.equals(magic, "ustar".toByteArray())) {
isTar = true
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
handleTar(it)
} else {
// Raw image
srcBoot = File(installDir, "boot.img").path
destFile = File(Config.downloadDirectory, "magisk_patched.img")
console.add("- Copying image to cache")
FileOutputStream(srcBoot).use { out -> it.copyTo(out) }
}
}
} catch (e: IOException) {
console.add("! Process error")
Timber.e(e)
return false
}
return true
}
protected fun patchBoot(): Boolean {
var isSigned = false
try {
SuFileInputStream(srcBoot).use {
isSigned = SignBoot.verifySignature(it, null)
if (isSigned) {
console.add("- Boot image is signed with AVB 1.0")
}
}
} catch (e: IOException) {
console.add("! Unable to check signature")
return false
}
if (!("KEEPFORCEENCRYPT=${Info.keepEnc} KEEPVERITY=${Info.keepVerity} " +
"RECOVERYMODE=${Info.recovery} sh update-binary " +
"sh boot_patch.sh $srcBoot").sh().isSuccess) {
return false
}
val job = Shell.sh(
"./magiskboot --cleanup",
"mv bin/busybox busybox",
"rm -rf magisk.apk bin boot.img update-binary",
"cd /")
val patched = File(installDir, "new-boot.img")
if (isSigned) {
console.add("- Signing boot image with test keys")
val signed = File(installDir, "signed.img")
try {
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
}
} catch (e: IOException) {
console.add("! Unable to sign image")
Timber.e(e)
return false
}
job.add("mv -f $signed $patched")
}
job.exec()
return true
}
protected fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false
if (!Info.keepVerity)
"patch_dtbo_image".sh()
return true
}
protected fun storeBoot(): Boolean {
val patched = SuFile.open(installDir, "new-boot.img")
try {
val os: OutputStream
if (isTar) {
os = TarOutputStream(destFile, true)
os.putNextEntry(newEntry(
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
patched.length()))
} else {
os = destFile.outputStream()
}
patched.suInputStream().use { it.copyTo(os); os.close() }
} catch (e: IOException) {
console.add("! Failed to output to $destFile")
Timber.e(e)
return false
}
patched.delete()
console.add("")
console.add("****************************")
console.add(" Output file is placed in ")
console.add(" $destFile ")
console.add("****************************")
return true
}
protected fun postOTA(): Boolean {
val bootctl = SuFile("/data/adb/bootctl")
try {
withStreams(service.fetchBootctl().blockingGet().byteStream(), bootctl.suOutputStream()) {
input, out -> input.copyTo(out)
}
} catch (e: IOException) {
console.add("! Unable to download bootctl")
Timber.e(e)
return false
}
"post_ota ${bootctl.parent}".sh()
console.add("***************************************")
console.add(" Next reboot will boot to second slot!")
console.add("***************************************")
return true
}
private fun String.sh() = Shell.sh(this).to(console, logs).exec()
private fun Array<String>.sh() = Shell.sh(*this).to(console, logs).exec()
private fun String.fsh() = ShellUtils.fastCmd(this)
private fun Array<String>.fsh() = ShellUtils.fastCmd(*this)
@WorkerThread
protected abstract fun operations(): Boolean
@MainThread
protected abstract fun onResult(success: Boolean)
fun exec() {
Single.fromCallable { operations() }.subscribeK { onResult(it) }
}
}

View File

@@ -0,0 +1,98 @@
package com.topjohnwu.magisk.tasks
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.data.network.GithubApiServices
import com.topjohnwu.magisk.model.entity.module.Repo
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.rxkotlin.toFlowable
import io.reactivex.schedulers.Schedulers
import se.ansman.kotshi.JsonSerializable
import timber.log.Timber
import java.net.HttpURLConnection
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.HashSet
class RepoUpdater(
private val api: GithubApiServices,
private val repoDB: RepoDao
) {
private fun loadRepos(repos: List<GithubRepoInfo>, cached: MutableSet<String>) =
repos.toFlowable().parallel().runOn(Schedulers.io()).map {
// Skip submission
if (it.id == "submission")
return@map
(repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?:
Repo(it.id)).runCatching {
update(it.pushDate)
repoDB.addRepo(this)
}.getOrElse { Timber.e(it) }
}.sequential()
private fun loadPage(
cached: MutableSet<String>,
page: Int = 1,
etag: String = ""
): Flowable<Unit> = api.fetchRepos(page, etag).flatMap {
it.error()?.also { throw it }
it.response()?.run {
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
return@run Flowable.error<Unit>(CachedException)
if (page == 1)
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
val flow = loadRepos(body()!!, cached)
if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) {
flow.mergeWith(loadPage(cached, page + 1))
} else {
flow
}
}
}
private fun forcedReload(cached: MutableSet<String>) =
cached.toFlowable().parallel().runOn(Schedulers.io()).map {
runCatching {
Repo(it).update()
}.getOrElse { Timber.e(it) }
}.sequential()
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
operator fun invoke(forced: Boolean = false) : Single<Unit> {
val cached = Collections.synchronizedSet(HashSet(repoDB.repoIDList))
return loadPage(cached, etag = repoDB.etagKey).doOnComplete {
repoDB.removeRepos(cached)
}.onErrorResumeNext { it: Throwable ->
if (it is CachedException) {
if (forced)
return@onErrorResumeNext forcedReload(cached)
} else {
Timber.e(it)
}
Flowable.empty()
}.ignoreElements().toSingleDefault(Unit)
}
object CachedException : Exception()
}
private val dateFormat: SimpleDateFormat =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}
@JsonSerializable
data class GithubRepoInfo(
val name: String,
val pushed_at: String
) {
val id get() = name
@Transient
val pushDate = dateFormat.parse(pushed_at)!!
}

View File

@@ -1,186 +0,0 @@
package com.topjohnwu.magisk.tasks;
import android.database.Cursor;
import android.util.Pair;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.Networking;
import com.topjohnwu.net.Request;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;
import java.util.Queue;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import io.reactivex.Single;
@Deprecated
public class UpdateRepos {
private static final DateFormat DATE_FORMAT;
static {
DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@NonNull
private final RepoDatabaseHelper repoDB;
private Set<String> cached;
private Queue<Pair<String, Date>> moduleQueue;
public UpdateRepos(@NonNull RepoDatabaseHelper repoDatabase) {
repoDB = repoDatabase;
}
private void runTasks(Runnable task) {
Future[] futures = new Future[App.THREAD_POOL.getMaximumPoolSize() - 1];
for (int i = 0; i < futures.length; ++i) {
futures[i] = App.THREAD_POOL.submit(task);
}
for (Future f : futures) {
while (true) {
try {
f.get();
} catch (InterruptedException e) {
continue;
} catch (ExecutionException ignored) {
}
break;
}
}
}
/* We sort repos by last push, which means that we only need to check whether the
* first page is updated to determine whether the online repo database is changed
*/
private boolean parsePage(int page) {
Request req = Networking.get(Utils.INSTANCE.fmt(Const.Url.REPO_URL, page + 1));
if (page == 0) {
String etag = Config.getEtagKey();
if (!etag.isEmpty())
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
}
Request.Result<JSONArray> res = req.execForJSONArray();
// JSON not updated
if (res.getCode() == HttpURLConnection.HTTP_NOT_MODIFIED)
return false;
// Network error
if (res.getResult() == null) {
cached.clear();
return true;
}
// Current page is the last page
if (res.getResult().length() == 0)
return true;
try {
for (int i = 0; i < res.getResult().length(); i++) {
JSONObject rawRepo = res.getResult().getJSONObject(i);
String id = rawRepo.getString("name");
Date date = DATE_FORMAT.parse(rawRepo.getString("pushed_at"));
moduleQueue.offer(new Pair<>(id, date));
}
} catch (JSONException | ParseException e) {
// Should not happen, but if exception occurs, page load fails
return false;
}
// Update ETAG
if (page == 0) {
String etag = res.getConnection().getHeaderField(Config.Key.ETAG_KEY);
if (etag != null) {
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
Config.setEtagKey(etag);
}
}
String links = res.getConnection().getHeaderField(Const.Key.LINK_KEY);
return links == null || !links.contains("next") || parsePage(page + 1);
}
private boolean loadPages() {
if (!parsePage(0))
return false;
runTasks(() -> {
while (true) {
Pair<String, Date> pair = moduleQueue.poll();
if (pair == null)
return;
Repo repo = repoDB.getRepo(pair.first);
try {
if (repo == null)
repo = new Repo(pair.first);
else
cached.remove(pair.first);
repo.update(pair.second);
repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
repoDB.removeRepo(pair.first);
}
}
});
return true;
}
private void fullReload() {
Cursor c = repoDB.getRawCursor();
runTasks(() -> {
while (true) {
Repo repo;
synchronized (c) {
if (!c.moveToNext())
return;
repo = new Repo(c);
}
try {
repo.update();
repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
repoDB.removeRepo(repo);
}
}
});
}
public Single<Boolean> exec(boolean force) {
return Single.fromCallable(() -> {
cached = Collections.synchronizedSet(repoDB.getRepoIDSet());
moduleQueue = new ConcurrentLinkedQueue<>();
if (loadPages()) {
// The leftover cached means they are removed from online repo
repoDB.removeRepo(cached);
} else if (force) {
fullReload();
}
return force; // not important
});
}
public Single<Boolean> exec() {
return exec(false);
}
}

View File

@@ -4,6 +4,7 @@ import android.content.Intent
import android.os.Bundle
import androidx.core.view.GravityCompat
import androidx.fragment.app.Fragment
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
@@ -20,7 +21,6 @@ import com.topjohnwu.magisk.ui.module.ReposFragment
import com.topjohnwu.magisk.ui.settings.SettingsFragment
import com.topjohnwu.magisk.ui.superuser.SuperuserFragment
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel
import kotlin.reflect.KClass
@@ -57,6 +57,10 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
checkHideSection()
setSupportActionBar(binding.mainInclude.mainToolbar)
viewModel.isConnected.addOnPropertyChangedCallback {
checkHideSection()
}
if (savedInstanceState == null) {
intent.getStringExtra(OPEN_SECTION)?.let {
onEventDispatched(Navigation.fromSection(it))
@@ -110,7 +114,7 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
menu.findItem(R.id.modulesFragment).isVisible =
Shell.rootAccess() && Info.magiskVersionCode >= 0
menu.findItem(R.id.reposFragment).isVisible =
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Info.magiskVersionCode >= 0)
(viewModel.isConnected.value && Shell.rootAccess() && Info.magiskVersionCode >= 0)
menu.findItem(R.id.logFragment).isVisible =
Shell.rootAccess()
menu.findItem(R.id.superuserFragment).isVisible =

View File

@@ -5,16 +5,11 @@ import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.get
open class SplashActivity : AppCompatActivity() {
@@ -38,7 +33,7 @@ open class SplashActivity : AppCompatActivity() {
private fun initAndStart() {
val pkg = Config.suManager
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
get<SettingsDao>().delete(Config.Key.SU_MANAGER)
Config.suManager = ""
Shell.su("pm uninstall $pkg").submit()
}
if (TextUtils.equals(pkg, packageName)) {
@@ -56,19 +51,11 @@ open class SplashActivity : AppCompatActivity() {
Notifications.setup(this)
// Schedule periodic update checks
Utils.scheduleUpdateCheck()
Utils.scheduleUpdateCheck(this)
// Setup shortcuts
Shortcuts.setup(this)
// Magisk working as expected
if (Shell.rootAccess() && Info.magiskVersionCode > 0) {
// Load repos
if (Networking.checkNetworkStatus(this)) {
get<UpdateRepos>().exec().subscribeK()
}
}
val intent = Intent(this, ClassMap[MainActivity::class.java])
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
DONE = true

View File

@@ -11,7 +11,6 @@ import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.preference.*
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.R
import org.koin.android.ext.android.inject
@@ -19,7 +18,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected val prefs: SharedPreferences by inject()
protected val app: App by inject()
protected val activity get() = requireActivity() as MagiskActivity<*, *>
override fun onCreateView(
inflater: LayoutInflater,
@@ -60,8 +59,4 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
else
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
}
protected fun <T: Preference> findPref(key: CharSequence): T {
return findPreference(key) as T
}
}

View File

@@ -22,6 +22,7 @@ import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.skoumal.teanity.view.TeanityActivity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.extensions.set
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
@@ -31,7 +32,7 @@ import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.utils.currentLocale
import timber.log.Timber
import kotlin.reflect.KClass
@@ -68,12 +69,12 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
config?.setLocale(LocaleManager.locale)
config?.setLocale(currentLocale)
super.applyOverrideConfiguration(config)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale))
super.attachBaseContext(LocaleManager.getLocaleContext(base))
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -17,21 +17,21 @@ import kotlin.reflect.KClass
abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityFragment<ViewModel, Binding>(), Navigator {
protected val magiskActivity get() = activity as MagiskActivity<*, *>
protected val activity get() = requireActivity() as MagiskActivity<*, *>
// We don't need nested fragments
override val baseFragments: List<KClass<Fragment>> = listOf()
override fun navigateTo(event: MagiskNavigationEvent) = magiskActivity.navigateTo(event)
override fun navigateTo(event: MagiskNavigationEvent) = activity.navigateTo(event)
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> magiskActivity.onBackPressed()
is BackPressEvent -> activity.onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(requireActivity())
is PermissionEvent -> magiskActivity.withPermissions(*event.permissions.toTypedArray()) {
is PermissionEvent -> activity.withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
@@ -42,10 +42,10 @@ abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBin
}
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
magiskActivity.withPermissions(*permissions, builder = builder)
activity.withPermissions(*permissions, builder = builder)
}
fun openLink(url: String) = magiskActivity.openUrl(url)
fun openLink(url: String) = activity.openUrl(url)
open fun onBackPressed(): Boolean = false

View File

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

View File

@@ -1,19 +1,35 @@
package com.topjohnwu.magisk.ui.flash
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
import com.topjohnwu.magisk.ui.base.MagiskActivity
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import java.io.File
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() {
override val layoutRes: Int = R.layout.activity_flash
override val viewModel: FlashViewModel by viewModel {
val uri = intent.data
val uri = intent.data ?: let { finish(); Uri.EMPTY }
val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri
val action = intent.getStringExtra(Const.Key.FLASH_ACTION) ?: let { finish();"" }
parametersOf(action, uri)
parametersOf(action, uri, additionalUri)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val id = intent.getIntExtra(Const.Key.DISMISS_ID, -1)
if (id != -1)
NotificationManagerCompat.from(this).cancel(id)
}
override fun onBackPressed() {
@@ -21,4 +37,56 @@ open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>(
super.onBackPressed()
}
companion object {
private fun intent(context: Context) = Intent(context, ClassMap[FlashActivity::class.java])
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
private fun intent(context: Context, file: File) = intent(context).setData(file.toUri())
private fun flashType(isSecondSlot: Boolean) =
if (isSecondSlot) Const.Value.FLASH_INACTIVE_SLOT else Const.Value.FLASH_MAGISK
/* Flashing is understood as installing / flashing magisk itself */
fun flashIntent(context: Context, file: File, isSecondSlot: Boolean, id : Int = -1)
= intent(context, file)
.putExtra(Const.Key.FLASH_ACTION, flashType(isSecondSlot))
.putExtra(Const.Key.DISMISS_ID, id)
fun flash(context: Context, file: File, isSecondSlot: Boolean, id: Int) =
context.startActivity(flashIntent(context, file, isSecondSlot, id))
/* Patching is understood as injecting img files with magisk */
fun patchIntent(context: Context, file: File, uri: Uri, id : Int = -1)
= intent(context, file)
.putExtra(Const.Key.FLASH_DATA, uri)
.putExtra(Const.Key.FLASH_ACTION, Const.Value.PATCH_FILE)
.putExtra(Const.Key.DISMISS_ID, id)
fun patch(context: Context, file: File, uri: Uri, id: Int) =
context.startActivity(patchIntent(context, file, uri, id))
/* Uninstalling is understood as removing magisk entirely */
fun uninstallIntent(context: Context, file: File, id : Int = -1)
= intent(context, file)
.putExtra(Const.Key.FLASH_ACTION, Const.Value.UNINSTALL)
.putExtra(Const.Key.DISMISS_ID, id)
fun uninstall(context: Context, file: File, id: Int) =
context.startActivity(uninstallIntent(context, file, id))
/* Installing is understood as flashing modules / zips */
fun installIntent(context: Context, file: File, id : Int = -1)
= intent(context, file)
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
.putExtra(Const.Key.DISMISS_ID, id)
fun install(context: Context, file: File, id: Int) =
context.startActivity(installIntent(context, file, id))
}
}

View File

@@ -13,14 +13,15 @@ import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.flash.FlashResultListener
import com.topjohnwu.magisk.model.flash.Flashing
import com.topjohnwu.magisk.model.flash.Patching
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File
@@ -28,7 +29,8 @@ import java.util.*
class FlashViewModel(
action: String,
uri: Uri?,
installer: Uri,
uri: Uri,
private val resources: Resources
) : MagiskViewModel(), FlashResultListener {
@@ -52,22 +54,21 @@ class FlashViewModel(
state = State.LOADING
val uri = uri ?: Uri.EMPTY
when (action) {
Const.Value.FLASH_ZIP -> Flashing
.Install(uri, outItems, logItems, this)
.Install(installer, outItems, logItems, this)
.exec()
Const.Value.UNINSTALL -> Flashing
.Uninstall(uri, outItems, logItems, this)
.Uninstall(installer, outItems, logItems, this)
.exec()
Const.Value.FLASH_MAGISK -> Patching
.Direct(outItems, logItems, this)
.Direct(installer, outItems, logItems, this)
.exec()
Const.Value.FLASH_INACTIVE_SLOT -> Patching
.SecondSlot(outItems, logItems, this)
.SecondSlot(installer, outItems, logItems, this)
.exec()
Const.Value.PATCH_FILE -> Patching
.File(uri, outItems, logItems, this)
.File(installer, uri, outItems, logItems, this)
.exec()
}
}
@@ -90,7 +91,7 @@ class FlashViewModel(
.map { now }
.map { it.toTime(timeFormatStandard) }
.map { Const.MAGISK_INSTALL_LOG_FILENAME.format(it) }
.map { File(Const.EXTERNAL_PATH, it) }
.map { File(Config.downloadDirectory, it) }
.map { file ->
file.bufferedWriter().use { writer ->
logItems.forEach {

View File

@@ -9,13 +9,13 @@ import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.toSingle
import com.topjohnwu.magisk.extensions.update
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.update
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber

View File

@@ -25,8 +25,22 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_magiskhide, menu)
menu.apply {
(findItem(R.id.app_search).actionView as? SearchView)
?.setOnQueryTextListener(this@MagiskHideFragment)
val query = viewModel.query.value
val searchItem = menu.findItem(R.id.app_search)
val searchView = searchItem.actionView as? SearchView
searchView?.run {
setOnQueryTextListener(this@MagiskHideFragment)
setQuery(query, false)
}
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView?.isIconified = false
} else {
searchItem.collapseActionView()
searchView?.isIconified = true
}
val showSystem = Config.showSystemApp

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