Compare commits

...

255 Commits
v19.4 ... v20.2

Author SHA1 Message Date
topjohnwu
e0d02a61a9 Add v7.5.0 changelog 2020-01-02 12:09:36 +08:00
topjohnwu
b3328a0ec2 Make sure shell command won't block 2020-01-02 02:19:56 +08:00
nikk gitanes
3c2041933f Remote focus fixes (classic UI) 2020-01-01 15:06:24 +08:00
孟武.尼德霍格.龍
e88b1cc443 繁體中文字串更新 2020-01-01 15:05:07 +08:00
Davy Defaud
71b05b18a0 Spelling, typographical and wording fixes
- spelling fixes
- typographical fixes : thin spaces before exclamation and interrogation marks, true apostrophes instead of single quotes, non-breaking spaces to avoid orphan words, etc.
- rewording for a better French translation
- fix various misinterpretation
2020-01-01 15:04:34 +08:00
Davy Defaud
b07b528e2a Add missing translation for "dling" string 2020-01-01 15:04:34 +08:00
Davy Defaud
1aeb6315ff Spelling & typographical fixes
- spelling fixes : complête → complète
- typographical fixes : thin spaces before exclamation and interrogation marks
- rewording for a better French translation
2020-01-01 15:04:34 +08:00
topjohnwu
1b4a3d2d9f More precise env detection in non-root 2020-01-01 14:19:24 +08:00
topjohnwu
3049a81c3b Update several scripts
- Update backup format as we might be patching multiple partitions
- Update uninstaller to remove files in persist (sepolicy.rule)
- Better handling for dtb/dtbo partition patching
2020-01-01 14:02:44 +08:00
topjohnwu
2db1e5cb74 Minor module related fixes 2019-12-30 13:21:39 +08:00
topjohnwu
78c64d39ec Add split command to magiskboot
Allow splitting image.*-dtb files to kernel and dtb
2019-12-30 13:04:39 +08:00
topjohnwu
46ba726232 Match exact "SKIPUNZIP=1" to prevent confusion 2019-12-29 15:16:04 +08:00
topjohnwu
eb26e62889 Update documentation 2019-12-29 03:28:03 +08:00
topjohnwu
7f667fed18 Allow customize.sh to skip unzip
Close #2184
2019-12-29 00:45:49 +08:00
topjohnwu
b2cb2b8b75 Reduce socket name length
Some detectors simply ban long abstract sockets
2019-12-28 21:27:55 +08:00
Zackptg5
d19f65ce4a Ignore twrp fstabs 2019-12-28 13:47:05 +08:00
topjohnwu
025b060506 Exclude META-INF in unzip 2019-12-28 02:33:35 +08:00
topjohnwu
7fa2625a03 Fix strings 2019-12-27 20:37:33 +08:00
topjohnwu
33d62d7f21 Handle sepolicy.rule when disable/remove in app 2019-12-27 19:03:45 +08:00
topjohnwu
b336655a79 Brand new module installer script
The new module installer script completely changes the way how module
installer zips are structured. More info will come later in docs.

The new installer script also supports installing sepolicy.rule to
persist partitions in order to make the module work on the next boot.
2019-12-27 17:53:27 +08:00
topjohnwu
3beffd84d6 Copy sepolicy rules to persist every boot 2019-12-22 03:44:07 -05:00
topjohnwu
02761f5f35 Fix French resources
Close #2169
2019-12-21 06:01:18 -05:00
topjohnwu
3b9f7885e0 Stop using chdir 2019-12-21 05:29:38 -05:00
topjohnwu
7668e45890 Cleanup legacy code 2019-12-17 17:15:31 -05:00
topjohnwu
695c8bc5d0 Detect package name for copying binaries
Close #2152
2019-12-17 16:38:12 -05:00
topjohnwu
06c42d05c3 Drop image based Magisk support 2019-12-15 21:01:12 -05:00
JoanVC100
404104208f Update ca-strings + Fixes 2019-12-15 19:49:12 -05:00
Rom
b4d0ad9713 Update French translation 2019-12-15 19:49:05 -05:00
topjohnwu
4f4f54a059 Remove unused code 2019-12-13 08:31:24 -05:00
topjohnwu
12fda29280 Add support for pre-init custom sepolicy patches
Close #1685
2019-12-13 06:05:12 -05:00
topjohnwu
af060b3132 General QoL changes 2019-12-13 00:37:06 -05:00
topjohnwu
8c500709e4 Remove SAR compatibility mode 2019-12-12 03:25:48 -05:00
topjohnwu
490e6a6f23 Add new API to load sepolicy rule file 2019-12-09 04:14:30 -05:00
topjohnwu
08177c3dd8 Mount persist partition mirror pre-init 2019-12-09 04:09:23 -05:00
topjohnwu
d22b9c26b6 Pull out common logic 2019-12-06 15:31:49 -05:00
topjohnwu
4bb8ad19cf Small init refactoring 2019-12-06 12:02:34 -05:00
topjohnwu
3e275b7dba Update a bunch of stuffs 2019-12-06 00:30:00 -05:00
topjohnwu
11b7076a43 Fix broken getxattr calls 2019-12-05 17:34:50 -05:00
Mevlüt TOPÇU
291c718ba2 Update Turkish language
Hi,

Update Turkish language

Merge please

Thanks
2019-12-05 17:21:42 -05:00
Hen Ry
fcd6071c57 Update de strings 2019-12-05 17:21:31 -05:00
topjohnwu
476b61c4c9 Support system_root with NVIDIA partition names
Fix #2063
2019-12-05 17:20:32 -05:00
topjohnwu
8cc5f096a2 Some minor changes 2019-12-05 17:20:32 -05:00
Alvin Wong
474d65207e Fix MagiskHide unmounting paths under /product
Fixes #2107
2019-12-03 05:42:10 -05:00
topjohnwu
03428329ef Add new verity and encryption patterns
Close #2118
2019-12-03 05:39:39 -05:00
topjohnwu
8d21988656 Support patching DTB/DTBO partition format 2019-12-02 04:34:21 -05:00
topjohnwu
72edbfc455 Some platforms do not like null Bundles 2019-11-25 19:09:54 -05:00
topjohnwu
276535dad6 Fix incorrect kmsg path
/proc/kmsg -> /dev/kmsg
2019-11-25 19:09:02 -05:00
topjohnwu
e373e59661 Make sure file descriptors are setup properly 2019-11-25 19:07:06 -05:00
topjohnwu
34bb18448c Fix compile errors 2019-11-23 17:18:55 -05:00
topjohnwu
01253f050a Use smart pointers 2019-11-23 04:57:52 -05:00
topjohnwu
5bee1c56a9 Properly use RAII to reduce complication 2019-11-22 03:01:49 -05:00
Lennoard
474cc7d56d Updated pt-BR strings (based on current
values/strings.xml)
2019-11-21 17:44:27 -05:00
topjohnwu
bffdedddb4 Fix fwrite/fread params 2019-11-21 17:43:31 -05:00
topjohnwu
fd72f658c0 Fix SQL command when creating magiskdb 2019-11-21 14:40:12 -05:00
topjohnwu
d3b5cf82d8 Small adjustments 2019-11-21 06:17:28 -05:00
topjohnwu
d26d804cc2 Migrate to generic stream implementation 2019-11-21 06:08:02 -05:00
topjohnwu
4f9a25ee89 Create generic streams on top of stdio
WIP
2019-11-20 21:48:49 -05:00
topjohnwu
bb9ce0e897 Make sepolicy dump more efficient 2019-11-20 03:47:15 -05:00
topjohnwu
d6fb9868bf Small sepolicy refactor and fixes 2019-11-19 05:20:18 -05:00
topjohnwu
9aff1a57d3 Cleanup headers 2019-11-19 02:04:47 -05:00
topjohnwu
7681fde4d0 Record mounts to be cleaned up in a vector 2019-11-19 00:16:20 -05:00
topjohnwu
d3b7b41927 Fix kmsg logging in magiskinit 2019-11-18 17:18:56 -05:00
topjohnwu
da159e4655 Better environment status detection 2019-11-16 17:38:10 -05:00
osm0sis
7f6a6016d6 magiskboot: add simple workaround for Samsung offset header variant
- some Samsung devices (e.g. Galaxy S5 SMG-900H) use a slightly different AOSP bootimg.h variant with `#define BOOT_NAME_SIZE 20` instead of 16
- since all known examples of these device images do not have anything in the NAME or CMDLINE fields, and the bootloader also accepts standard AOSP images, simply offset the SHA1/SHA256 detection by 4 bytes to avoid false positives from these images, remain an equally effective detection shortcut, and ensure a proper SHA1 checksum on repack

aosp-dtbhdt2-4offhash-seandroid-256sig-samsung_gs5-smg900h-boot.img
UNPACK CHECKSUM [00000000b11580f7d20f70297cdc31e02626def0356c82b90000000000000000]
REPACK CHECKSUM [73b18751202e56c433f89dfd1902c290eaf4eef3e167fcf03b814b59a5e984b6]
AIK CHECKSUM    [b11580f7d20f70297cdc31e02626def0356c82b9000000000000000000000000]

This patch should result in a `magiskboot unpack -n boot.img; magiskboot repack boot.img` new-boot.img matching the AIK CHECKSUM above.
2019-11-16 03:23:49 -05:00
Nick
44ed0a3279 Update RU strings
Minor improvements and fixes
2019-11-16 03:23:32 -05:00
dark-basic
9964e1bb8e Update strings.xml 2019-11-16 03:23:20 -05:00
Viktor De Pasquale
8b8f725499 Fixed log items not being refreshed
Close #2079
2019-11-16 03:20:43 -05:00
topjohnwu
bab856bce2 Move biometric settings higher in the list 2019-11-16 03:19:25 -05:00
topjohnwu
3d285b91c6 Use ContextCompat 2019-11-15 11:01:39 -05:00
vvb2060
1dc531930d Update zh-rCN translation 2019-11-15 10:55:51 -05:00
Ilya Kushnir
3d3345acac Update RU strings 2019-11-15 10:55:41 -05:00
topjohnwu
b29f0ca4d1 Support using BiometricPrompt 2019-11-14 05:42:39 -05:00
topjohnwu
576efbdc1b Move su logs out of magiskdb 2019-11-14 00:01:06 -05:00
topjohnwu
a7f0510a3e Update build.gradle 2019-11-13 17:17:21 -05:00
topjohnwu
2ef088cb60 Update RepoDao 2019-11-13 13:23:58 -05:00
topjohnwu
7c320b6fc4 Reformat RxJava extensions 2019-11-13 13:22:51 -05:00
topjohnwu
5a4c82b860 Bump stub version 2019-11-13 03:01:38 -05:00
孟武.尼德霍格.龍
9b297b752e Update strings.xml 2019-11-13 02:37:35 -05:00
vvb2060
1d6ba58ccd Update zh-rCN translation 2019-11-13 02:37:10 -05:00
topjohnwu
1542447822 Reuse buffer 2019-11-13 02:36:45 -05:00
topjohnwu
a6f0aff659 Only store UID for current user 2019-11-13 02:36:34 -05:00
topjohnwu
171ddab32b Reorganize code handling su requests 2019-11-12 03:20:07 -05:00
topjohnwu
2aee0b0be0 Refactor code for handling MagiskDB 2019-11-11 15:46:02 -05:00
vvb2060
817cdf7113 fix multiuser owner_managed mode 2019-11-11 14:12:26 -05:00
topjohnwu
1a38f25bd9 Properly invoke method 2019-11-10 14:59:19 -05:00
topjohnwu
ad40e53349 Update hacks 2019-11-09 18:17:16 -05:00
topjohnwu
a2ddf362d8 Make a.a not extend AppComponentFactory
Fix #2053
2019-11-09 16:13:15 -05:00
Ilya Kushnir
65eca31635 Updating RU translation 2019-11-09 04:40:10 -05:00
osm0sis
8b0b4a2c39 SignBoot: also catch empty streamed signature as indicating not signed
- compare against new byte[] array as a quick tell, since when streaming from a partition with an unsigned image "signature" would of course read without issue but then remain filled by zero padding, resulting in the following:
    java.io.IOException: unexpected end-of-contents marker
        at org.bouncycastle.asn1.ASN1InputStream.readObject(Unknown Source:14)
        at com.topjohnwu.signing.SignBoot$BootSignature.<init>(SignBoot.java:235)
        at com.topjohnwu.signing.SignBoot.verifySignature(SignBoot.java:144)
        at com.topjohnwu.signing.BootSigner.main(BootSigner.java:15)
        at a.a.main(a.java:20)
2019-11-09 04:39:41 -05:00
topjohnwu
c0216c0653 Get XMLs directly 2019-11-08 02:59:09 -05:00
topjohnwu
61de63a518 Cleanup manifest 2019-11-08 02:15:30 -05:00
topjohnwu
d952cc2327 Properly solve the connection problem 2019-11-07 17:41:59 -05:00
topjohnwu
46447f7cfd Proper string buffer size 2019-11-05 01:46:46 -05:00
topjohnwu
a6e62e07a2 Sort modules ignore case
Close #2024
2019-11-04 17:14:18 -05:00
topjohnwu
b1d25e0503 Reuse ALPHANUM 2019-11-04 15:42:40 -05:00
topjohnwu
25c557248c Use ContentProvider call method for communication
Previously, we use either BroadcastReceivers or Activities to receive
messages from our native daemon, but both have their own downsides.
Some OEMs blocks broadcasts if the app is not running in the background,
regardless of who the caller is. Activities on the other hand, despite
working 100% of the time, will steal the focus of the current foreground
app, even though we are just doing some logging and showing a toast.
In addition, since stubs for hiding Magisk Manager is introduced, our
only communication method is left with the broadcast option, as
only broadcasting allows targeting a specific package name, not a
component name (which will be obfuscated in the case of stubs).

To make sure root requests will work on all devices, Magisk had to do
some experiments every boot to test whether broadcast is deliverable or
not. This makes the whole thing even more complicated then ever.

So lets take a look at another kind of component in Android apps:
ContentProviders. It is a vital part of Android's ecosystem, and as far
as I know no OEMs will block requests to ContentProviders (or else
tons of functionality will break catastrophically). Starting at API 11,
the system supports calling a specific method in ContentProviders,
optionally sending extra data along with the method call. This is
perfect for the native daemon to start a communication with Magisk
Manager. Another cool thing is that we no longer need to know the
component name of the reciever, as ContentProviders identify themselves
with an "authority" name, which in Magisk Manager's case is tied to the
package name. We already have a mechanism to keep track of our current
manager package name, so this works out of the box.

So yay! No more flaky broadcast tests, no more stupid OEMs blocking
broadcasts for some bizzare reasons. This method should in theory
work on almost all devices and situations.
2019-11-04 14:32:28 -05:00
topjohnwu
472cde29b8 Allow non supported Magisk to use Magisk Manager
Close #1576
2019-11-04 03:24:27 -05:00
linar10
73525d19e9 Update strings.xml 2019-11-03 17:15:17 -05:00
topjohnwu
26618f8d73 Don't do broadcast tests from app
Running broadcast tests from the app does not accurately verifies
whether the broadcasts can be delivered when the app is not running in
the foreground, which is why we are running the test.

The only sane way to verify broadcasts is to trigger the broadcast test
directly from the daemon on boot complete. If it is not deliverable,
then activity mode shall be chosen.

In the meantime, cleanup AndroidManifest.xml
2019-11-03 17:01:09 -05:00
topjohnwu
6f7c13b814 Refactor JarMap 2019-11-03 04:45:35 -05:00
osm0sis
e7d668502c SignBoot: improve error catching/reporting
- `!= remain` shouldn't indicate "not signed", it should indicate a read error as with `!= hdr.length`
- attempt to catch unsigned images at signature read, before they make it to `BootSignature bootsig = new BootSignature(signature);` and result in the following:
    java.io.IOException: unexpected end-of-contents marker
            at org.bouncycastle.asn1.ASN1InputStream.readObject(Unknown Source:14)
            at com.topjohnwu.signing.SignBoot$BootSignature.<init>(SignBoot.java:230)
            at com.topjohnwu.signing.SignBoot.verifySignature(SignBoot.java:139)
            at com.topjohnwu.signing.BootSigner.main(BootSigner.java:15)
            at a.a.main(a.java:20)
2019-11-03 04:22:21 -05:00
osm0sis
6fd357962f scripts: fix signing in recovery with addon.d-v1
- change to $TMPDIR in addon.d.sh since recovery addon.d-v1 backup + restore leaves you in /tmp/addon.d which the restore then deletes, which would break $BOOTSIGNER execution with the following:
    libc: Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0 in tid 1078 (main), pid 1078 (main)
    Segmentation fault
- also move $BOOTSIGNER execution to after `cd $MAGISKBIN` to ensure it's in a working directory in all cases
- addon.d.sh data mount wasn't doing anything since /data has to already be mounted for the script to be running, so move it into /system/addon.d/99-magisk.sh stub script where it might be useful on recoveries that don't mount /data initially

Fixes #2013
2019-11-03 03:00:08 -05:00
topjohnwu
0c9feedb37 Support restarting app when obfuscated 2019-11-03 02:55:22 -05:00
Vladimír Kubala
14ba002cbc Update Slovak translation 2019-11-02 11:59:20 -04:00
topjohnwu
7da97489cc Add v7.4.0 release notes 2019-11-02 01:24:56 -04:00
topjohnwu
a9f11b28c8 Fix busybox scripts again 2019-11-02 01:16:54 -04:00
topjohnwu
b31d986c8d Update scripts 2019-11-02 00:41:51 -04:00
Oliver Cervera
2dad751889 Update Italian translation
- updated existing strings based on english updates
- added new strings
2019-11-02 00:28:07 -04:00
osm0sis
c85b1c56af signing: fixes for bootimg hdr_v1 and hdr_v2
- increase SignBoot bootimg header version maximum from 4 to 8 (upstream AOSP is already at 3) and make a variable for future ease
- hdr read size of 1024 bytes was too small as hdr_v1 and hdr_v2 have increased the used header page areas to 1632 and 1648 bytes, respectively, so raise this to the minimum page size of 2048 and also make a variable for future ease
- do not return "not signed" for all caught exceptions, show StackTrace for future debugging then still return false for script purposes
- correct "test keys" boot image signing strings (scripts and app) to "verity keys"
2019-11-02 00:27:56 -04:00
osm0sis
6dd34aec47 scripts: refactor and major addon.d fixes
- remove redundant addon.d.sh script bits that were covered elsewhere ($TMPDIR in util_functions.sh, find_dtbo_image in patch_dtbo_image)
- refactor addon.d.sh and flash_script.sh for simplicity and readability, and put common flashing script in util_functions.sh (as patch_boot_image), which should greatly help avoid them getting out of sync going forward and fixes compressing ramdisk support and post-patch cleanup for addon.d
- add check_data to addon.d.sh since moving stock_boot* and stock_dtbo* backups depend on it and so weren't occuring with addon.d
- fix find_manager_apk with working fallback for recovery addon.d execution (where `magisk --sqlite` will not work for hidden Manager), Manager DynAPK hiding, and print a useful log warning if an APK can't be found
2019-11-02 00:27:56 -04:00
topjohnwu
4cd154675f Random dname 2019-11-01 18:52:37 -04:00
Viktor De Pasquale
d8d72f92b3 Fixed policy toggle being impossible to cancel 2019-11-01 14:47:59 +01:00
topjohnwu
a30f5b175f Fix busybox makefiles 2019-11-01 09:38:01 -04:00
topjohnwu
8277896ca1 Make sure uninstall.sh is executed on remove 2019-11-01 03:07:12 -04:00
topjohnwu
493068c073 Attempt to rescan zygote multiple times
Close #1654
2019-11-01 02:12:28 -04:00
topjohnwu
f4299fbea8 Update BusyBox to 1.31.1 2019-10-31 18:11:10 -04:00
topjohnwu
10ce11d671 Fix config/locale issues
Close #1977
2019-10-31 17:13:06 -04:00
topjohnwu
0f34457a10 Directly store strings in viewmodel 2019-10-31 15:33:13 -04:00
topjohnwu
34c65e13bc Fix strings
Close #2012
2019-10-31 12:39:54 -04:00
John Wu
17a77e2577 Shortcut booleans 2019-10-31 02:44:25 -04:00
John Wu
0f219e5ae6 Better argument parsing logic 2019-10-31 02:44:25 -04:00
osm0sis
353c3c7d81 magiskboot: add unpack -n to help with repack validity tests
- support unpack without decompression to allow easy testing of magiskboot's header, structure and hashing handling by comparing repack checksum versus origbootimg
- make -n first to match repack
2019-10-31 02:44:25 -04:00
Rom
0a89edf3b0 Update French translation 2019-10-31 02:04:00 -04:00
topjohnwu
e7155837d7 Make sure magisk daemon won't get killed by init
According to this comment in #1880:
https://github.com/topjohnwu/Magisk/issues/1880#issuecomment-546657588

If Linux recycled our PPID, and coincidentally the process that reused
the PPID is root, AND init wants to kill the whole process group,
magiskd will get killed as a result.

There is no real way to block a SIGKILL signal, so we simply make sure
our daemon PID is the process group leader by renaming the directory.

Close #1880
2019-10-31 01:57:47 -04:00
topjohnwu
31e003bda5 Fix bug in version detection 2019-10-30 05:24:22 -04:00
topjohnwu
490e4d3180 Target the proper channel in stub 2019-10-30 05:00:52 -04:00
topjohnwu
dc9f69bab0 Minor changes 2019-10-30 04:15:53 -04:00
topjohnwu
fdf04f77f2 Send bitmap to notifications and shortcuts
On API 23+, the platform unifies the way to handle drawable
resources across processes: all drawables can be passed via Icon.
This allows us to send raw bitmap to the system without the need to
specify a resource ID. This means that we are allowed to NOT include
these drawable resources within our stub APK, since our full APK can
draw the images programmatically and send raw bitmaps to the system.
2019-10-30 01:02:53 -04:00
topjohnwu
5e87483f34 Move addAssetPath to shared 2019-10-29 07:37:19 -04:00
topjohnwu
f7aa451591 Update strings 2019-10-29 07:36:50 -04:00
topjohnwu
321d11c2c6 Move Mapping class 2019-10-29 07:21:14 -04:00
topjohnwu
ee447bc4ce Improve Keygen yet again 2019-10-26 21:11:32 -04:00
Nathan Muccino
31153e4366 Minor grammatical changes
The plural form of the words 'documentation' and 'following' are used very rarely if ever and I don't believe that they should be used in this particular context.
2019-10-26 19:26:27 -04:00
topjohnwu
7693024c29 Replace general resources with platform 2019-10-26 19:23:57 -04:00
Mevlüt TOPÇU
9628700a2f Update Turkish language
Hi,

Merge please

Thanks
2019-10-26 19:03:52 -04:00
Taras
38576173cb Update Ukrainian translation 2019-10-26 19:03:37 -04:00
topjohnwu
19a769c12e Update dependencies 2019-10-26 19:02:11 -04:00
topjohnwu
3c1db7d2f7 Fix some A/B devices unable to boot into recovery
Some newer recovery ramdisk no longer have /sbin/recovery.
Add /system/bin/recovery as an additional indication for recovery.

Close #1920
2019-10-26 17:12:35 -04:00
topjohnwu
626507093a Don't need to wrap another layer of context 2019-10-26 15:37:12 -04:00
topjohnwu
588b3d14a3 Fix typo 2019-10-24 15:37:32 -04:00
vvb2060
815efa7791 Update zh-rCN translation 2019-10-24 13:04:36 -04:00
topjohnwu
97a691ce2f Improve keygen for signing repackaged manager 2019-10-24 13:04:15 -04:00
topjohnwu
9d948f2c2b Temporary disable verification when hiding app
For some reason, Google Play Protect randomly blocks our self-signed
repackaged Magisk Manager APKs. Since we are root, the sky is our
limit, so yeah, disable package verification temporarily when installing
patched APKs, LOLz

Close #1979
2019-10-24 12:23:03 -04:00
topjohnwu
0b87108174 Move things around 2019-10-24 05:21:42 -04:00
topjohnwu
7fc7809cfc More precise channel targeting 2019-10-24 04:25:05 -04:00
topjohnwu
c30be20e49 Minor CachedValue fix 2019-10-24 04:02:01 -04:00
topjohnwu
25c64db0a1 Treat outdated stub as outdated manager 2019-10-24 03:54:16 -04:00
topjohnwu
676e9c6593 Provide upgrade path for stubs 2019-10-24 02:47:40 -04:00
topjohnwu
d459859361 Show stub version 2019-10-24 00:54:40 -04:00
topjohnwu
2be0cef446 Add proper intent filters to stub 2019-10-23 17:55:26 -04:00
topjohnwu
294db93fde Copy instead of move
We might be copying from CE to DE storage, which cannot be moved
2019-10-23 17:20:55 -04:00
topjohnwu
7f971f7173 Make sure our constructor is preserved 2019-10-23 07:51:32 -04:00
topjohnwu
5c7b59524d Fix strings 2019-10-23 07:15:28 -04:00
topjohnwu
5133e5910e Don't relaunch app immediately 2019-10-23 07:12:00 -04:00
osm0sis
1512c350df magiskboot: add SPRD dt support
- per https://github.com/USA-RedDragon/sprd-mkbootimg-tools/blob/master/dtbtool.c
- touch up hdr and table naming to be more uniform
2019-10-23 06:58:31 -04:00
あきと ミズキト
a5fc7891a6 build: Addressed file not found 2019-10-23 06:57:47 -04:00
Abhishek Dubey
3eb9633231 Add Hindi Translation 2019-10-23 06:53:46 -04:00
onevt
ac67b48247 Fix swedish translation typo 2019-10-23 06:53:07 -04:00
topjohnwu
81b65ea646 Exclude stub id mappings from git 2019-10-23 06:45:47 -04:00
topjohnwu
45c1f6bc27 Fix restore manager when running as stub 2019-10-23 06:43:08 -04:00
topjohnwu
0d31e5c8b1 Properly migrate update channels when repackaging 2019-10-23 06:41:25 -04:00
topjohnwu
6378abf454 Make stub support directBootAware 2019-10-23 05:52:32 -04:00
topjohnwu
f8fcaadb5b Hide manager with stub if feasible 2019-10-23 05:50:06 -04:00
topjohnwu
0b5fd3ee76 Only allow hide/restore app if connected 2019-10-23 05:43:01 -04:00
topjohnwu
d010cb7e42 Update stub 2019-10-23 05:19:54 -04:00
topjohnwu
71136d7347 Manually trigger broadcast tests if necessary 2019-10-22 16:04:20 -04:00
topjohnwu
a18c552ddf Guard env state behind cached objects 2019-10-22 15:37:55 -04:00
topjohnwu
9656878ef3 Actually apply the input name 2019-10-22 05:06:17 -04:00
Viktor De Pasquale
7ded7de39a Added custom dialog for setting app's name after repackaging 2019-10-22 04:52:19 -04:00
topjohnwu
0f74e89b44 Introduce component agnostic communication
Usually, the communication between native and the app is done via
sending intents to either broadcast or activity. These communication
channels are for launching root requests dialogs, sending root request
notifications (the toast you see when an app gained root access), and
root request logging.

Sending intents by am (activity manager) usually requires specifying
the component name in the format of <pkg>/<class name>. This means parts
of Magisk Manager cannot be randomized or else the native daemon is
unable to know where to send data to the app.

On modern Android (not sure which API is it introduced), it is possible
to send broadcasts to a package, not a specific component. Which
component will receive the intent depends on the intent filter declared
in AndroidManifest.xml. Since we already have a mechanism in native code
to keep track of the package name of Magisk Manager, this makes it
perfect to pass intents to Magisk Manager that have components being
randomly obfuscated (stub APKs).

There are a few caveats though. Although this broadcasting method works
perfectly fine on AOSP and most systems, there are OEMs out there
shipping ROMs blocking broadcasts unexpectedly. In order to make sure
Magisk works in all kinds of scenarios, we run actual tests every boot
to determine which communication method should be used.

We have 3 methods in total, ordered in preference:
1. Broadcasting to a package
2. Broadcasting to a specific component
3. Starting a specific activity component

Method 3 will always work on any device, but the downside is anytime
a communication happens, Magisk Manager will steal foreground focus
regardless of whether UI is drawn. Method 1 is the only way to support
obfuscated stub APKs. The communication test will test method 1 and 2,
and if Magisk Manager is able to receive the messages, it will then
update the daemon configuration to use whichever is preferable. If none
of the broadcasts can be delivered, then the fallback method 3 will be
used.
2019-10-21 13:59:04 -04:00
topjohnwu
953c40b083 Allow upgrade Magisk daemon in emulator 2019-10-21 13:58:57 -04:00
topjohnwu
271b0287d8 Pass in stub version just in case 2019-10-20 17:47:55 -04:00
topjohnwu
96a8a2a8b8 Make SuRequest default to Translucent.NoTitleBar
Close #1959
2019-10-20 17:35:38 -04:00
topjohnwu
75306f658f Revert "Drop API 17 (Android 4.2) support"
Turns out that we cannot use AndroidKeystore anyways, so we don't
actually need to drop API 17. Revert this change.
2019-10-20 07:13:03 -04:00
topjohnwu
325d9a0b86 Generate keys for signing hidden Magisk Manager 2019-10-20 06:56:33 -04:00
topjohnwu
a02493fbaa Workaround R8 bug 2019-10-19 05:44:56 -04:00
topjohnwu
9c27d691dd Drop API 17 (Android 4.2) support 2019-10-19 03:11:54 -04:00
topjohnwu
935bd01f59 Post process release APKs 2019-10-17 18:02:31 -04:00
topjohnwu
eeb5d669f6 Assign signing keystore location in config 2019-10-17 16:20:01 -04:00
topjohnwu
78daa2eb62 Do not use string resources for app label
This not only simplifies hiding stub APKs (no resource IDs involved),
but also opens the opportunity to allow users to customize whatever
app name they want after it is hidden.
2019-10-17 04:47:46 -04:00
topjohnwu
40eda05a30 Make main app fully independent from the stub
- Skip 0x7f01XXXX - 0x7f05XXXX resource IDs in the main app; they are
reserved for stub resources
- Support sending additional data from host to guest
- Use resource mapping passed from host when they are being sent
to the system framework (notifications and shortcuts)
2019-10-17 02:55:42 -04:00
topjohnwu
9f9de8c43b Obfuscate WorkManager components
Remove unused components and hack the context sent into WorkManager
2019-10-16 17:03:55 -04:00
topjohnwu
a910c8ccd8 Support stub APK upgrades 2019-10-16 05:07:29 -04:00
topjohnwu
43bda2d4a4 Allow component classname obfuscation 2019-10-16 04:38:31 -04:00
topjohnwu
c7033dd757 Allow injecting custom channel URL for debug 2019-10-16 01:54:59 -04:00
topjohnwu
5673a9bace Move system accessible resources to shared 2019-10-15 05:49:23 -04:00
topjohnwu
34ff764515 Stabilize resource IDs 2019-10-15 04:37:12 -04:00
topjohnwu
1b3a009da7 Remove unused WorkManager components 2019-10-15 04:36:09 -04:00
topjohnwu
a49002bb2c Reorganize string resources 2019-10-15 03:33:22 -04:00
Omar Kharrab
7342fc2307 Update Arabic translation 2019-10-15 02:57:43 -04:00
topjohnwu
9867a3bd60 Pedantic boot_img_hdr multi-version support 2019-10-15 01:46:29 -04:00
topjohnwu
5ffb9eaa5b Support loading Magisk Manager from stub on 9.0+
In the effort of preventing apps from crawling APK contents across the
whole installed app list to detect Magisk Manager, the solution here
is to NOT install the actual APK into the system, but instead
dynamically load the full app at runtime by a stub app. The full APK
will be stored in the application's private internal data where
non-root processes cannot read or scan.

The basis of this implementation is the class "AppComponentFactory"
that is introduced in API 28. If assigned, the system framework will
delegate app component instantiation to our custom implementation,
which allows us to do all sorts of crazy stuffs, in our case dynamically
load classes and create objects that does not exist in our APK.

There are a few challenges to achieve our goal though. First, Java
ClassLoaders follow the "delegation pattern", which means class loading
resolution will first be delegated to the parent loader before we get
a chance to do anything. This includes DexClassLoader, which is what
we will be using to load DEX files at runtime. This is a problem
because our stub app and full app share quite a lot of class names.
A custom ClassLoader, DynamicClassLoader, is created to overcome this
issue: it will always load classes in its current dex path before
delegating it to the parent.

Second, all app components (with the exception of runtime
BroadcastReceivers) are required to be declared in AndroidManifest.xml.
The full Magisk Manager has quite a lot of components (including
those from WorkManager and Room). The solution is to copy the complete
AndroidManifest.xml from the full app to the stub, and our
AppComponentFactory is responsible to construct the proper objects or
return dummy implementations in case the full APK isn't downloaded yet.

Third, other than classes, all resources required to run the full app
are also not bundled with the stub APK. We have to call an internal API
`AssetManager.addAssetPath(String)` to add our downloaded full APK into
AssetManager in order to access resources within our full app. That
internal API has existed forever, and is whitelisted from restricted
API access on modern Android versions, so it is pretty safe to use.

Fourth, on the subject of resources, some resources are not just being
used by our app at runtime. Resources such as the app icon, app label,
launch theme, basically everything referred in AndroidManifest.xml,
are used by the system to display the app properly. The system get these
resources via resource IDs and direct loading from the installed APK.
This subset of resources would have to be copied into the stub to make
the app work properly.

Fifth, resource IDs are used all over the place in XMLs and Java code.
The resource IDs in the stub and full app cannot missmatch, or
somewhere, either it be the system or AssetManager, will refer to the
incorrect resource. The full app will have to include all resources in
the stub, and all of them have to be assigned to the exact same IDs in
both APKs. To achieve this, we use AAPT2's "--emit-ids" option to dump
the resource ID mapping when building the stub, and "--stable-ids" when
building the full APK to make sure all overlapping resources in full
and stub are always assigned to the same ID.

Finally, both stub and full app have to work properly independently.
On 9.0+, the stub will have to first launch an Activity to download
the full APK before it can relaunch into the full app. On pre-9.0, the
stub should behave as it always did: download and prompt installation
to upgrade itself to full Magisk Manager. In the full app, the goal
is to introduce minimal intrusion to the code base to make sure this
whole thing is maintainable in the future. Fortunately, the solution
ends up pretty slick: all ContextWrappers in the app will be injected
with custom Contexts. The custom Contexts will return our patched
Resources object and the ClassLoader that loads itself, which will be
DynamicClassLoader in the case of running as a delegate app.
By directly patching the base Context of ContextWrappers (which covers
tons of app components) and in the Koin DI, the effect propagates deep
into every aspect of the code, making this change basically fully
transparent to almost every piece of code in full Magisk Manager.

After this commit, the stub app is able to properly download and launch
the full app, with most basic functionalities working just fine.
Do not expect Magisk Manager upgrades and hiding (repackaging) to
work properly, and some other minor issues might pop up.
This feature is still in the early WIP stages.
2019-10-14 03:49:17 -04:00
topjohnwu
b05b688267 Fix issues in stub APK 2019-10-12 03:58:45 -04:00
Simon Shi
f3d7f85063 Fix incorrect link path for /sbin/.core 2019-10-12 01:00:15 -04:00
topjohnwu
de969a9dab Downgrade recyclerview 2019-10-12 00:53:04 -04:00
topjohnwu
59fd38bbf8 Add v7.3.5 changelog 2019-10-11 16:12:32 -04:00
topjohnwu
06dc6df270 Allow dalvik runtime to load snet 2019-10-11 03:58:04 -04:00
topjohnwu
ff8460b361 Update dependencies 2019-10-11 03:29:55 -04:00
topjohnwu
674d272eaa Support pre-5.0 without GMS
Fix #1912
2019-10-11 01:46:15 -04:00
topjohnwu
c3e00c279d Legacy adb shell does not have uname 2019-10-11 01:45:06 -04:00
dark-basic
175d920c94 Update strings.xml
I'M BACK.
New translations were added.
2019-10-10 17:17:09 -04:00
topjohnwu
04920883ea Change code for handling tar files 2019-10-10 15:07:45 -04:00
topjohnwu
5e44b0b9d5 Use raw literals for scripts 2019-10-09 17:38:45 -04:00
topjohnwu
23c1a1dab8 Some code reorganizing 2019-10-09 16:01:21 -04:00
topjohnwu
f5d054b93c Add support for PXA DTBs 2019-10-08 23:49:21 -04:00
topjohnwu
d25ae5e0a9 Add __attribute__((packed)) just in case 2019-10-08 16:55:25 -04:00
topjohnwu
c42a51dcbb Add support to patch DTBH DTBs
Apparently, Qualcomm is not the only on creating weird DTB formats,
Samsung also have their own DTBH format for Exynos platforms.

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

Merge please

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

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

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

The solution:
- guarantee a consistent addon.d-v2 experience for users across ROMs when rooted with Magisk by making update_engine permissive as Lineage has
- hopefully ROMs can work together to come up with something standard for unrooted addon.d-v2 function
2019-09-23 07:55:25 -04:00
topjohnwu
695e7e6da0 Create product mirror if /system/product exist 2019-09-23 06:52:24 -04:00
topjohnwu
4cd4bfa1d7 Add ':' to allowed characters for magiskhide process name 2019-09-22 16:17:51 -04:00
topjohnwu
16b400964b Update vars for 2SI 2019-09-22 06:45:23 -04:00
topjohnwu
cf2d02c0dd Don't wipe ramdisk when A-only SAR 2019-09-22 06:17:54 -04:00
topjohnwu
0fcd0de0d1 Fix potential crash when traversing cpio entries 2019-09-22 06:15:19 -04:00
topjohnwu
748a35774f Support patching fstab in ramdisk for A-only 2SI 2019-09-22 05:30:04 -04:00
topjohnwu
a52a3e38ed Change some class names 2019-09-22 05:20:51 -04:00
topjohnwu
ee0cef06a6 Add support for A-only 2SI 2019-09-22 05:15:31 -04:00
topjohnwu
0e5a113a0c Support patching mnt_point in fstab in dtb 2019-09-22 04:17:15 -04:00
topjohnwu
a1ccd44013 Change MagiskBoot patch behavior
Use environment variables to toggle configurations for patching ramdisk
2019-09-21 05:55:23 -04:00
topjohnwu
4d91e50d6d Update dtb patch to not use in-place modification 2019-09-21 05:30:04 -04:00
topjohnwu
120668c7bc Revise dtb commands CLI 2019-09-20 03:53:58 -04:00
topjohnwu
d81ccde569 Pretty print dtb content 2019-09-20 03:05:14 -04:00
topjohnwu
e8581b4adb Fix links in docs 2019-09-19 05:48:21 -04:00
topjohnwu
19906575a3 Update v7.3.4 changelogs 2019-09-19 05:29:45 -04:00
383 changed files with 10319 additions and 7720 deletions

4
.gitignore vendored
View File

@@ -2,8 +2,8 @@ out
*.zip
*.jks
*.apk
config.prop
update.sh
/config.prop
/update.sh
# Built binaries
native/out

View File

@@ -30,13 +30,12 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
## Translations
Default string resources for Magisk Manager are scattered throughout
Default string resources for Magisk Manager and its stub APK are located here:
- `app/src/main/res/values/strings.xml`
- `stub/src/main/res/values/strings.xml`
- `shared/src/main/res/values/strings.xml`
Translate each and place them in the respective locations (`<module>/src/main/res/values-<lang>/strings.xml`).
Translate each and place them in the respective locations (`[module]/src/main/res/values-[lang]/strings.xml`).
## Signature Verification

View File

@@ -35,13 +35,12 @@ android {
}
packagingOptions {
exclude '/META-INF/*.version'
exclude '/META-INF/*.kotlin_module'
exclude '/META-INF/rxkotlin.properties'
exclude '/META-INF/**'
exclude '/androidsupportmultidexversion.txt'
exclude '/org/bouncycastle/**'
exclude '/kotlin/**'
exclude '/kotlinx/**'
exclude '/okhttp3/**'
}
kotlinOptions {
@@ -60,38 +59,50 @@ dependencies {
implementation 'com.github.topjohnwu:jtar:1.0.0'
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.1.0'
implementation "ru.noties.markwon:core:${vMarkwon}"
implementation "ru.noties.markwon:html:${vMarkwon}"
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
implementation 'io.reactivex.rxjava2:rxjava:2.2.13'
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib:${vKotlin}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${vKotlin}"
def vBAdapt = '3.1.1'
def bindingAdapter = 'me.tatarka.bindingcollectionadapter2:bindingcollectionadapter'
implementation "${bindingAdapter}:${vBAdapt}"
implementation "${bindingAdapter}-recyclerview:${vBAdapt}"
def vMarkwon = '4.2.0'
implementation "io.noties.markwon:core:${vMarkwon}"
implementation "io.noties.markwon:html:${vMarkwon}"
implementation "io.noties.markwon:image:${vMarkwon}"
implementation 'com.caverock:androidsvg:1.4'
def vLibsu = '2.5.1'
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
def vKoin = "2.0.1"
def vKoin = '2.0.1'
implementation "org.koin:koin-core:${vKoin}"
implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = '2.6.1'
def vRetrofit = '2.6.2'
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.2'
def vOkHttp = '3.12.6'
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
def vMoshi = "1.8.0"
def vMoshi = '1.9.2'
implementation "com.squareup.moshi:moshi:${vMoshi}"
def vKotshi = "2.0.1"
def vKotshi = '2.0.2'
implementation "se.ansman.kotshi:api:${vKotshi}"
kapt "se.ansman.kotshi:compiler:${vKotshi}"
@@ -100,20 +111,25 @@ dependencies {
replacedBy('com.github.topjohnwu:room-runtime')
}
}
def vRoom = "2.1.0"
def vRoom = '2.2.2'
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
implementation "androidx.room:room-rxjava2:${vRoom}"
kapt "androidx.room:room-compiler:${vRoom}"
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}"
def vNav = '2.1.0'
implementation "androidx.navigation:navigation-fragment-ktx:${vNav}"
implementation "androidx.navigation:navigation-ui-ktx:${vNav}"
implementation 'androidx.biometric:biometric:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-alpha03'
implementation 'androidx.preference:preference:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.work:work-runtime:2.2.0'
implementation 'androidx.transition:transition:1.2.0-rc01'
implementation 'androidx.transition:transition:1.3.0-rc02'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha10'
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'com.google.android.material:material:1.2.0-alpha02'
}

View File

@@ -29,10 +29,14 @@
}
# DelegateWorker
-keep,allowobfuscation class * extends com.topjohnwu.magisk.model.worker.DelegateWorker
-keep,allowobfuscation class * extends com.topjohnwu.magisk.base.DelegateWorker
# BootSigner
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
-keep class a.a { *; }
# Workaround R8 bug
-keep,allowobfuscation class com.topjohnwu.magisk.model.receiver.GeneralReceiver
-keepclassmembers class a.e { *; }
# Strip logging
-assumenosideeffects class timber.log.Timber.Tree { *; }

5
app/res-ids.txt Normal file
View File

@@ -0,0 +1,5 @@
com.topjohnwu.magisk:color/xxxxxxxx = 0x7f010000
com.topjohnwu.magisk:drawable/xxxxxxxx = 0x7f020000
com.topjohnwu.magisk:string/xxxxxxxx = 0x7f030000
com.topjohnwu.magisk:style/xxxxxxxx = 0x7f040000
com.topjohnwu.magisk:xml/xxxxxxxx = 0x7f050000

View File

@@ -1,59 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
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:name="a.e"
android:allowBackup="true"
android:theme="@style/MagiskTheme"
android:usesCleartextTraffic="true"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
<!-- Activities -->
<activity
android:name="a.b"
android:configChanges="orientation|screenSize"
android:exported="true" />
<!-- Splash -->
<activity
android:name="a.c"
android:configChanges="orientation|screenSize"
android:exported="true"
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="a.f"
android:configChanges="keyboardHidden|orientation|screenSize"
android:screenOrientation="nosensor"
android:theme="@style/MagiskTheme.Flashing" />
<!-- Main -->
<activity android:name="a.b" />
<!-- Flashing -->
<activity android:name="a.f" />
<!-- Superuser -->
<activity
android:name="a.m"
android:directBootAware="true"
android:excludeFromRecents="true"
android:exported="false"
android:theme="@style/MagiskTheme.SU" />
android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:ignore="AppLinkUrlError">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<!-- Receiver -->
<receiver
android:name="a.h"
android:directBootAware="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.REBOOT" />
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
<intent-filter>
@@ -64,16 +59,30 @@
</intent-filter>
</receiver>
<!-- Service -->
<service android:name="a.j"
android:exported="false" />
<!-- DownloadService -->
<service android:name="a.j" />
<!-- Hardcode GMS version -->
<meta-data
android:name="com.google.android.gms.version"
android:value="12451000" />
<!-- Initialize WorkManager on-demand -->
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
<!-- We don't invalidate Room -->
<service
android:name="androidx.room.MultiInstanceInvalidationService"
tools:node="remove"/>
<!-- We don't use Device Credentials -->
<activity
android:name="androidx.biometric.DeviceCredentialHandlerActivity"
tools:node="remove" />
</application>
</manifest>

View File

@@ -3,11 +3,18 @@ package a;
import com.topjohnwu.magisk.utils.PatchAPK;
import com.topjohnwu.signing.BootSigner;
import androidx.annotation.Keep;
public class a {
@Keep
public class a extends BootSigner {
@Deprecated
public static boolean patchAPK(String in, String out, String pkg) {
return PatchAPK.patch(in, out, pkg);
}
public static boolean patchAPK(String in, String out, String pkg, String label) {
return PatchAPK.patch(in, out, pkg, label);
}
public static void main(String[] args) throws Exception {
BootSigner.main(args);
}
}

View File

@@ -3,5 +3,11 @@ package a;
import com.topjohnwu.magisk.App;
public class e extends App {
/* stub */
public e() {
super();
}
public e(Object o) {
super(o);
}
}

View File

@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.topjohnwu.magisk.model.worker.DelegateWorker;
import com.topjohnwu.magisk.base.DelegateWorker;
import java.lang.reflect.ParameterizedType;

View File

@@ -6,57 +6,85 @@ import android.content.res.Configuration
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import androidx.room.Room
import androidx.work.WorkManager
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.data.database.SuLogDatabase
import com.topjohnwu.magisk.data.database.SuLogDatabase_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.extensions.unwrap
import com.topjohnwu.magisk.utils.RootInit
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.utils.updateConfig
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
open class App : Application() {
open class App() : Application() {
constructor(o: Any) : this() {
Info.stub = DynAPK.load(o)
}
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.addInitializers(RootInit::class.java)
Shell.Config.setTimeout(2)
FileProvider.callHandler = SuHandler
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
RepoDatabase::class.java -> RepoDatabase_Impl()
SuLogDatabase::class.java -> SuLogDatabase_Impl()
else -> null
}
}
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
// Basic setup
if (BuildConfig.DEBUG)
MultiDex.install(base)
Timber.plant(Timber.DebugTree())
// Some context magic
val app: Application
val impl: Context
if (base is Application) {
app = base
impl = base.baseContext
} else {
app = this
impl = base
}
val wrapped = impl.wrap()
super.attachBaseContext(wrapped)
// Normal startup
startKoin {
androidContext(this@App)
androidContext(wrapped)
modules(koinModules)
}
ResourceMgr.init(impl)
app.registerActivityLifecycleCallbacks(get<ActivityTracker>())
WorkManager.initialize(impl.wrapJob(), androidx.work.Configuration.Builder().build())
}
registerActivityLifecycleCallbacks(get<ActivityTracker>())
Networking.init(base)
LocaleManager.setLocale(this)
// This is required as some platforms expect ContextImpl
override fun getBaseContext(): Context {
return super.getBaseContext().unwrap()
}
override fun onConfigurationChanged(newConfig: Configuration) {
resources.updateConfig(newConfig)
if (!isRunningAsStub)
super.onConfigurationChanged(newConfig)
LocaleManager.setLocale(this)
}
}

View File

@@ -1,26 +0,0 @@
package com.topjohnwu.magisk
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
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
object ClassMap {
private val map = mapOf(
App::class.java to a.e::class.java,
MainActivity::class.java to a.b::class.java,
SplashActivity::class.java to a.c::class.java,
FlashActivity::class.java to a.f::class.java,
UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java,
DownloadService::class.java to a.j::class.java,
SuRequestActivity::class.java to a.m::class.java
)
operator fun <T : Class<*>>get(c: Class<*>): T {
return map.getOrElse(c) { throw IllegalArgumentException() } as T
}
}

View File

@@ -11,9 +11,8 @@ import com.topjohnwu.magisk.data.repository.DBConfig
import com.topjohnwu.magisk.di.Protected
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.model.preference.PreferenceModel
import com.topjohnwu.magisk.utils.FingerprintHelper
import com.topjohnwu.magisk.utils.BiometricHelper
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.io.SuFile
@@ -32,8 +31,9 @@ object Config : PreferenceModel, DBConfig {
const val ROOT_ACCESS = "root_access"
const val SU_MULTIUSER_MODE = "multiuser_mode"
const val SU_MNT_NS = "mnt_ns"
const val SU_BIOMETRIC = "su_biometric"
const val SU_MANAGER = "requester"
const val SU_FINGERPRINT = "su_fingerprint"
const val KEYSTORE = "keystore"
// prefs
const val SU_REQUEST_TIMEOUT = "su_request_timeout"
@@ -48,6 +48,7 @@ object Config : PreferenceModel, DBConfig {
const val REPO_ORDER = "repo_order"
const val SHOW_SYSTEM_APP = "show_system"
const val DOWNLOAD_PATH = "download_path"
const val BOOT_ID = "boot_id"
// system state
const val MAGISKHIDE = "magiskhide"
@@ -97,9 +98,16 @@ object Config : PreferenceModel, DBConfig {
}
private val defaultChannel =
if (Utils.isCanary) Value.CANARY_DEBUG_CHANNEL
if (Utils.isCanary) {
if (BuildConfig.DEBUG)
Value.CANARY_DEBUG_CHANNEL
else
Value.CANARY_CHANNEL
}
else Value.DEFAULT_CHANNEL
var bootId by preference(Key.BOOT_ID, "")
var downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
@@ -121,18 +129,25 @@ object Config : PreferenceModel, DBConfig {
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
var suBiometric by dbSettings(Key.SU_BIOMETRIC, false)
var suManager by dbStrings(Key.SU_MANAGER, "", true)
var keyStoreRaw by dbStrings(Key.KEYSTORE, "", 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 {
private const val SU_FINGERPRINT = "su_fingerprint"
fun initialize() = prefs.also {
if (it.getBoolean(SU_FINGERPRINT, false)) {
suBiometric = true
}
}.edit {
parsePrefs(this)
if (!prefs.contains(Key.UPDATE_CHANNEL))
putString(Key.UPDATE_CHANNEL, defaultChannel.toString())
// Legacy stuff
remove(SU_FINGERPRINT)
// Get actual state
putBoolean(Key.COREONLY, Const.MAGISK_DISABLE_FILE.exists())
@@ -141,13 +156,16 @@ object Config : PreferenceModel, DBConfig {
putString(Key.ROOT_ACCESS, rootMode.toString())
putString(Key.SU_MNT_NS, suMntNamespaceMode.toString())
putString(Key.SU_MULTIUSER_MODE, suMultiuserMode.toString())
putBoolean(Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
putBoolean(Key.SU_BIOMETRIC, BiometricHelper.isEnabled)
}.also {
if (!prefs.contains(Key.UPDATE_CHANNEL))
prefs.edit().putString(Key.UPDATE_CHANNEL, defaultChannel.toString()).apply()
}
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 input = SuFileInputStream(config)
val parser = Xml.newPullParser()
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
parser.setInput(input, "UTF-8")
@@ -198,9 +216,10 @@ object Config : PreferenceModel, DBConfig {
fun export() {
// Flush prefs to disk
prefs.edit().commit()
val context = get<Context>(Protected)
val xml = File(
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
"${packageName}_preferences.xml"
"${context.filesDir.parent}/shared_prefs",
"${context.packageName}_preferences.xml"
)
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
}

View File

@@ -13,8 +13,8 @@ object Const {
// Versions
const val SNET_EXT_VER = 13
const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
const val SNET_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
const val BOOTCTL_REVISION = "a6c47f86f10b310358afa9dbe837037dd5d561df"
// Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml"
@@ -22,8 +22,11 @@ object Const {
const val MANAGER_CONFIGS = ".tmp.magisk.config"
val USER_ID = Process.myUid() / 100000
object MagiskVersion {
const val MIN_SUPPORT = 18000
object Version {
const val MIN_VERSION = "v18.0"
const val MIN_VERCODE = 18000
const val CONNECT_MODE = 20100
const val PROVIDER_CONNECT = 20102
}
object ID {

View File

@@ -0,0 +1,155 @@
@file:Suppress("DEPRECATION")
package com.topjohnwu.magisk
import android.annotation.SuppressLint
import android.app.job.JobInfo
import android.app.job.JobScheduler
import android.app.job.JobWorkItem
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.res.AssetManager
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.RequiresApi
import com.topjohnwu.magisk.extensions.forceGetDeclaredField
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
import com.topjohnwu.magisk.ui.SplashActivity
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.refreshLocale
import com.topjohnwu.magisk.utils.updateConfig
fun AssetManager.addAssetPath(path: String) {
DynAPK.addAssetPath(this, path)
}
fun Context.wrap(global: Boolean = true): Context
= if (global) GlobalResContext(this) else ResContext(this)
fun Context.wrapJob(): Context = object : GlobalResContext(this) {
override fun getApplicationContext(): Context {
return this
}
@SuppressLint("NewApi")
override fun getSystemService(name: String): Any? {
return if (!isRunningAsStub) super.getSystemService(name) else
when (name) {
Context.JOB_SCHEDULER_SERVICE ->
JobSchedulerWrapper(super.getSystemService(name) as JobScheduler)
else -> super.getSystemService(name)
}
}
}
fun Class<*>.cmp(pkg: String): ComponentName {
val name = ClassMap[this].name
return ComponentName(pkg, Info.stub?.classToComponent?.get(name) ?: name)
}
inline fun <reified T> Context.intent() = Intent().setComponent(T::class.java.cmp(packageName))
private open class GlobalResContext(base: Context) : ContextWrapper(base) {
open val mRes: Resources get() = ResourceMgr.resource
override fun getResources(): Resources {
return mRes
}
override fun getClassLoader(): ClassLoader {
return javaClass.classLoader!!
}
override fun createConfigurationContext(config: Configuration): Context {
return ResContext(super.createConfigurationContext(config))
}
}
private class ResContext(base: Context) : GlobalResContext(base) {
override val mRes by lazy { base.resources.patch() }
private fun Resources.patch(): Resources {
updateConfig()
if (isRunningAsStub)
assets.addAssetPath(ResourceMgr.resApk)
return this
}
}
object ResourceMgr {
lateinit var resource: Resources
lateinit var resApk: String
fun init(context: Context) {
resource = context.resources
refreshLocale()
if (isRunningAsStub) {
resApk = DynAPK.current(context).path
resource.assets.addAssetPath(resApk)
}
}
}
@RequiresApi(28)
private class JobSchedulerWrapper(private val base: JobScheduler) : JobScheduler() {
override fun schedule(job: JobInfo): Int {
return base.schedule(job.patch())
}
override fun enqueue(job: JobInfo, work: JobWorkItem): Int {
return base.enqueue(job.patch(), work)
}
override fun cancel(jobId: Int) {
base.cancel(jobId)
}
override fun cancelAll() {
base.cancelAll()
}
override fun getAllPendingJobs(): List<JobInfo> {
return base.allPendingJobs
}
override fun getPendingJob(jobId: Int): JobInfo? {
return base.getPendingJob(jobId)
}
private fun JobInfo.patch(): JobInfo {
// We need to swap out the service of JobInfo
val name = service.className
val component = ComponentName(
service.packageName,
Info.stub!!.classToComponent[name] ?: name)
javaClass.forceGetDeclaredField("service")?.set(this, component)
return this
}
}
object ClassMap {
private val map = mapOf(
App::class.java to a.e::class.java,
MainActivity::class.java to a.b::class.java,
SplashActivity::class.java to a.c::class.java,
FlashActivity::class.java to a.f::class.java,
UpdateCheckService::class.java to a.g::class.java,
GeneralReceiver::class.java to a.h::class.java,
DownloadService::class.java to a.j::class.java,
SuRequestActivity::class.java to a.m::class.java,
ProcessPhoenix::class.java to a.r::class.java
)
operator fun get(c: Class<*>) = map.getOrElse(c) { c }
}

View File

@@ -1,26 +1,77 @@
package com.topjohnwu.magisk
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.model.entity.UpdateInfo
import com.topjohnwu.magisk.utils.CachedValue
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
import java.io.FileInputStream
import java.io.IOException
val isRunningAsStub get() = Info.stub != null
object Info {
var magiskVersionCode = -1
val envRef = CachedValue { loadState() }
var magiskVersionString = ""
var remote = UpdateInfo()
val env by envRef // Local
var remote = UpdateInfo() // Remote
var stub: DynAPK.Data? = null // Stub
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
val isConnected by lazy {
KObservableField(false).also { field ->
ReactiveNetwork.observeNetworkConnectivity(get())
.subscribeK {
field.value = it.available()
}
}
}
val isNewReboot by lazy {
try {
FileInputStream("/proc/sys/kernel/random/boot_id").bufferedReader().use {
val id = it.readLine()
if (id != Config.bootId) {
Config.bootId = id
true
} else {
false
}
}
} catch (e: IOException) {
false
}
}
private fun loadState() = runCatching {
val str = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
val code = ShellUtils.fastCmd("magisk -V").toInt()
val hide = Shell.su("magiskhide --status").exec().isSuccess
Env(str, code, hide)
}.getOrElse { Env() }
class Env(
val magiskVersionString: String = "",
code: Int = -1,
hide: Boolean = false
) {
val magiskHide get() = Config.magiskHide
val magiskVersionCode = when (code) {
in Int.MIN_VALUE..Const.Version.MIN_VERCODE -> -1
else -> if(Shell.rootAccess()) code else -1
}
val isUnsupported = code > 0 && code < Const.Version.MIN_VERCODE
val isActive = magiskVersionCode >= 0
init {
Config.magiskHide = hide
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,17 @@
package com.topjohnwu.magisk.base
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import com.topjohnwu.magisk.wrap
import org.koin.core.KoinComponent
abstract class BaseReceiver : BroadcastReceiver(), KoinComponent {
final override fun onReceive(context: Context, intent: Intent?) {
onReceive(context.wrap() as ContextWrapper, intent)
}
abstract fun onReceive(context: ContextWrapper, intent: Intent?)
}

View File

@@ -0,0 +1,12 @@
package com.topjohnwu.magisk.base
import android.app.Service
import android.content.Context
import com.topjohnwu.magisk.wrap
import org.koin.core.KoinComponent
abstract class BaseService : Service(), KoinComponent {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
}

View File

@@ -1,4 +1,4 @@
package com.topjohnwu.magisk.model.worker
package com.topjohnwu.magisk.base
import android.content.Context
import android.net.Network

View File

@@ -1,32 +1,27 @@
package com.topjohnwu.magisk.ui.base
package com.topjohnwu.magisk.base.viewmodel
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.base.BaseActivity
import com.topjohnwu.magisk.extensions.doOnSubscribeUi
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.KObservableField
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import com.topjohnwu.magisk.Info.isConnected as gIsConnected
abstract class MagiskViewModel(
abstract class BaseViewModel(
initialState: State = State.LOADING
) : LoadingViewModel(initialState) {
val isConnected = KObservableField(true)
init {
ReactiveNetwork.observeNetworkConnectivity(get())
.subscribeK { isConnected.value = it.available() }
.add()
val isConnected = object : KObservableField<Boolean>(gIsConnected.value, gIsConnected) {
override fun get(): Boolean {
return gIsConnected.value
}
}
fun withView(action: Activity.() -> Unit) {
fun withView(action: BaseActivity<*, *>.() -> Unit) {
ViewActionEvent(action).publish()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,33 +0,0 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.toLog
import com.topjohnwu.magisk.model.entity.toMap
import java.util.concurrent.TimeUnit
class LogDao : BaseDao() {
override val table = DatabaseDefinition.Table.LOG
fun deleteOutdated(
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
) = query<Delete> {
condition {
lessThan("time", suTimeout.toString())
}
}.ignoreElement()
fun deleteAll() = query<Delete> {}.ignoreElement()
fun fetchAll() = query<Select> {
orderBy("time", Order.DESC)
}.flattenAsFlowable { it }
.map { it.toLog() }
.toList()
fun put(log: MagiskLog) = query<Insert> {
values(log.toMap())
}.ignoreElement()
}

View File

@@ -3,7 +3,10 @@ package com.topjohnwu.magisk.data.database
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.data.database.magiskdb.BaseDao
import com.topjohnwu.magisk.data.database.magiskdb.Delete
import com.topjohnwu.magisk.data.database.magiskdb.Replace
import com.topjohnwu.magisk.data.database.magiskdb.Select
import com.topjohnwu.magisk.extensions.now
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toMap
@@ -16,7 +19,7 @@ class PolicyDao(
private val context: Context
) : BaseDao() {
override val table: String = DatabaseDefinition.Table.POLICY
override val table: String = Table.POLICY
fun deleteOutdated(
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)

View File

@@ -4,8 +4,14 @@ import androidx.room.*
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.entity.module.Repo
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
abstract class RepoDatabase : RoomDatabase() {
abstract fun repoDao() : RepoDao
}
@Dao
abstract class RepoDao {
abstract class RepoDao(private val db: RepoDatabase) {
val repoIDList get() = getRepoID().map { it.id }
@@ -15,13 +21,10 @@ abstract class RepoDao {
}
var etagKey: String
set(etag) = addEtagRaw(RepoEtag(0, etag))
set(value) = addEtagRaw(RepoEtag(0, value))
get() = etagRaw()?.key.orEmpty()
fun clear() {
clearRepos()
clearEtag()
}
fun clear() = db.clearAllTables()
@Query("SELECT * FROM repos ORDER BY last_update DESC")
protected abstract fun getReposDateOrder(): List<Repo>
@@ -52,12 +55,6 @@ abstract class RepoDao {
@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(

View File

@@ -1,11 +0,0 @@
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,10 +1,13 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
import com.topjohnwu.magisk.data.database.magiskdb.Delete
import com.topjohnwu.magisk.data.database.magiskdb.Replace
import com.topjohnwu.magisk.data.database.magiskdb.Select
class SettingsDao : BaseDao() {
override val table = DatabaseDefinition.Table.SETTINGS
override val table = Table.SETTINGS
fun delete(key: String) = query<Delete> {
condition { equals("key", key) }

View File

@@ -1,10 +1,13 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.data.database.magiskdb.BaseDao
import com.topjohnwu.magisk.data.database.magiskdb.Delete
import com.topjohnwu.magisk.data.database.magiskdb.Replace
import com.topjohnwu.magisk.data.database.magiskdb.Select
class StringDao : BaseDao() {
override val table = DatabaseDefinition.Table.STRINGS
override val table = Table.STRINGS
fun delete(key: String) = query<Delete> {
condition { equals("key", key) }

View File

@@ -0,0 +1,36 @@
package com.topjohnwu.magisk.data.database
import androidx.room.*
import com.topjohnwu.magisk.model.entity.MagiskLog
import io.reactivex.Completable
import io.reactivex.Single
import java.util.*
@Database(version = 1, entities = [MagiskLog::class])
abstract class SuLogDatabase : RoomDatabase() {
abstract fun suLogDao(): SuLogDao
}
@Dao
abstract class SuLogDao(private val db: SuLogDatabase) {
private val twoWeeksAgo =
Calendar.getInstance().apply { add(Calendar.WEEK_OF_YEAR, -2) }.timeInMillis
fun deleteAll() = Completable.fromAction { db.clearAllTables() }
fun fetchAll() = deleteOutdated().andThen(fetch())
@Query("SELECT * FROM logs ORDER BY time DESC")
protected abstract fun fetch(): Single<MutableList<MagiskLog>>
@Insert
abstract fun insert(log: MagiskLog): Completable
@Query("DELETE FROM logs WHERE time < :timeout")
protected abstract fun deleteOutdated(
timeout: Long = twoWeeksAgo
): Completable
}

View File

@@ -1,15 +0,0 @@
package com.topjohnwu.magisk.data.database.base
abstract class BaseDao {
abstract val table: String
inline fun <reified Builder : MagiskQueryBuilder> query(builder: Builder.() -> Unit) =
Builder::class.java.newInstance()
.apply { table = this@BaseDao.table }
.apply(builder)
.toString()
.let { MagiskQuery(it) }
.query()
}

View File

@@ -1,30 +0,0 @@
package com.topjohnwu.magisk.data.database.base
import androidx.annotation.AnyThread
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
object DatabaseDefinition {
object Table {
const val POLICY = "policies"
const val LOG = "logs"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
}
@AnyThread
fun MagiskQuery.query() = query.su()
fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
fun String.su() = suRaw().map { it.toMap() }
fun List<String>.toMap() = map { it.split(Regex("\\|")) }
.map { it.toMapInternal() }
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()

View File

@@ -1,5 +0,0 @@
package com.topjohnwu.magisk.data.database.base
inline class MagiskQuery(private val _query: String) {
val query get() = "magisk --sqlite '$_query'"
}

View File

@@ -0,0 +1,44 @@
package com.topjohnwu.magisk.data.database.magiskdb
import androidx.annotation.StringDef
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
abstract class BaseDao {
object Table {
const val POLICY = "policies"
const val LOG = "logs"
const val SETTINGS = "settings"
const val STRINGS = "strings"
}
@StringDef(Table.POLICY, Table.LOG, Table.SETTINGS, Table.STRINGS)
@Retention(AnnotationRetention.SOURCE)
annotation class TableStrict
@TableStrict
abstract val table: String
inline fun <reified Builder : Query.Builder> query(builder: Builder.() -> Unit = {}) =
Builder::class.java.newInstance()
.apply { table = this@BaseDao.table }
.apply(builder)
.toString()
.let { Query(it) }
.query()
}
fun Query.query() = query.su()
private fun String.suRaw() = Single.fromCallable { Shell.su(this).exec().out }
private fun String.su() = suRaw().map { it.toMap() }
private fun List<String>.toMap() = map { it.split(Regex("\\|")) }
.map { it.toMapInternal() }
private fun List<String>.toMapInternal() = map { it.split("=", limit = 2) }
.filter { it.size == 2 }
.map { Pair(it[0], it[1]) }
.toMap()

View File

@@ -1,27 +1,17 @@
package com.topjohnwu.magisk.data.database.base
package com.topjohnwu.magisk.data.database.magiskdb
import androidx.annotation.StringDef
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
interface MagiskQueryBuilder {
class Query(private val _query: String) {
val query get() = "magisk --sqlite '$_query'"
interface Builder {
val requestType: String
var table: String
companion object {
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
Builder::class.java.newInstance()
.apply(builder)
.toString()
.let {
MagiskQuery(it)
}
}
}
class Delete : MagiskQueryBuilder {
class Delete : Query.Builder {
override val requestType: String = "DELETE FROM"
override var table = ""
@@ -36,7 +26,7 @@ class Delete : MagiskQueryBuilder {
}
}
class Select : MagiskQueryBuilder {
class Select : Query.Builder {
override val requestType: String get() = "SELECT $fields FROM"
override lateinit var table: String
@@ -69,7 +59,7 @@ class Replace : Insert() {
override val requestType: String = "REPLACE INTO"
}
open class Insert : MagiskQueryBuilder {
open class Insert : Query.Builder {
override val requestType: String = "INSERT INTO"
override lateinit var table: String
@@ -137,19 +127,11 @@ class Condition {
}
}
class Order {
@set:OrderStrict
var order = DESC
var field = ""
companion object {
object Order {
const val ASC = "ASC"
const val DESC = "DESC"
}
}
@StringDef(ASC, DESC)
@StringDef(Order.ASC, Order.DESC)
@Retention(AnnotationRetention.SOURCE)
annotation class OrderStrict

View File

@@ -19,10 +19,10 @@ interface GithubRawServices {
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/release.json")
@GET("$MAGISK_FILES/canary/release.json")
fun fetchCanaryUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
@GET("$MAGISK_FILES/canary/debug.json")
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
@GET

View File

@@ -59,8 +59,8 @@ class DBBoolSettings(
val base = DBSettingsValue(name, if (default) 1 else 0)
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean
= base.getValue(thisRef, property) != 0
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Boolean =
base.getValue(thisRef, property) != 0
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Boolean) =
base.setValue(thisRef, property, if (value) 1 else 0)

View File

@@ -1,38 +1,36 @@
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.data.database.SuLogDao
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
import io.reactivex.Single
import java.util.concurrent.TimeUnit
class LogRepository(
private val logDao: LogDao
private val logDao: SuLogDao
) {
fun fetchLogs() = logDao.fetchAll()
.map { it.sortByDescending { it.date.time }; it }
.map { it.wrap() }
fun fetchLogs() = logDao.fetchAll().map { it.wrap() }
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
.filter { it.isNotEmpty() }
fun fetchMagiskLogs() = Single.fromCallable {
Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").exec().out
}.flattenAsFlowable { it }.filter { it.isNotEmpty() }
fun clearLogs() = logDao.deleteAll()
fun clearOutdated() = logDao.deleteOutdated()
fun clearMagiskLogs() = Shell.su("echo -n > " + Const.MAGISK_LOG)
.toSingle()
.map { it.exec() }
fun clearMagiskLogs() = Completable.fromAction {
Shell.su("echo -n > ${Const.MAGISK_LOG}").exec()
}
fun put(log: MagiskLog) = logDao.put(log)
fun insert(log: MagiskLog) = logDao.insert(log)
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
val day = TimeUnit.DAYS.toMillis(1)
return groupBy { it.date.time / day }
return groupBy { it.time / day }
.map { WrappedMagiskLog(it.key * day, it.value) }
}

View File

@@ -3,7 +3,6 @@ package com.topjohnwu.magisk.data.repository
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.extensions.getLabel
import com.topjohnwu.magisk.extensions.packageName
@@ -29,7 +28,7 @@ class MagiskRepository(
else -> throw IllegalArgumentException()
}.flatMap {
// If remote version is lower than current installed, try switching to beta
if (it.magisk.versionCode < Info.magiskVersionCode
if (it.magisk.versionCode < Info.env.magiskVersionCode
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
apiRaw.fetchBetaUpdate()
@@ -57,7 +56,7 @@ class MagiskRepository(
.toList()
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
Shell.su("magiskhide --${isEnabled.state} $packageName $process").submit()
private val Boolean.state get() = if (this) "add" else "rm"

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ 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.utils.RxBus
import org.koin.core.qualifier.named
import org.koin.dsl.module

View File

@@ -8,12 +8,11 @@ import org.koin.dsl.module
val databaseModule = module {
single { LogDao() }
single { PolicyDao(get()) }
single { SettingsDao() }
single { StringDao() }
single { createRepoDatabase(get()) }
single { get<RepoDatabase>().repoDao() }
single { createRepoDatabase(get()).repoDao() }
single { createSuLogDatabase(get(Protected)).suLogDao() }
single { RepoUpdater(get(), get()) }
}
@@ -21,3 +20,8 @@ fun createRepoDatabase(context: Context) =
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
.fallbackToDestructiveMigration()
.build()
fun createSuLogDatabase(context: Context) =
Room.databaseBuilder(context, SuLogDatabase::class.java, "sulogs.db")
.fallbackToDestructiveMigration()
.build()

View File

@@ -1,11 +1,18 @@
package com.topjohnwu.magisk.di
import android.content.Context
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.GithubApiServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.net.Networking
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
import io.noties.markwon.Markwon
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.ImagesPlugin
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.koin.dsl.module
@@ -16,14 +23,15 @@ import retrofit2.converter.scalars.ScalarsConverterFactory
import se.ansman.kotshi.KotshiJsonAdapterFactory
val networkingModule = module {
single { createOkHttpClient() }
single { createMoshiConverterFactory() }
single { createRetrofit(get(), get()) }
single { createOkHttpClient(get()) }
single { createRetrofit(get()) }
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
single { createMarkwon(get(), get()) }
}
fun createOkHttpClient(): OkHttpClient {
@Suppress("DEPRECATION")
fun createOkHttpClient(context: Context): OkHttpClient {
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
@@ -33,6 +41,10 @@ fun createOkHttpClient(): OkHttpClient {
builder.addInterceptor(httpLoggingInterceptor)
}
if (!Networking.init(context)) {
builder.sslSocketFactory(NoSSLv3SocketFactory())
}
return builder.build()
}
@@ -43,13 +55,10 @@ fun createMoshiConverterFactory(): MoshiConverterFactory {
return MoshiConverterFactory.create(moshi)
}
fun createRetrofit(
okHttpClient: OkHttpClient,
converterFactory: MoshiConverterFactory
): Retrofit.Builder {
fun createRetrofit(okHttpClient: OkHttpClient): Retrofit.Builder {
return Retrofit.Builder()
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(converterFactory)
.addConverterFactory(createMoshiConverterFactory())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(okHttpClient)
}
@@ -63,3 +72,12 @@ inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseU
.build()
.create(T::class.java)
}
fun createMarkwon(context: Context, okHttpClient: OkHttpClient): Markwon {
return Markwon.builder(context)
.usePlugin(HtmlPlugin.create())
.usePlugin(ImagesPlugin.create {
it.addSchemeHandler(OkHttpNetworkSchemeHandler.create(okHttpClient))
})
.build()
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package com.topjohnwu.magisk.extensions
import android.content.ComponentName
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo
@@ -8,13 +10,33 @@ import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.*
import android.content.res.Configuration
import android.content.res.Resources
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.LayerDrawable
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.provider.OpenableColumns
import com.topjohnwu.magisk.utils.FileProvider
import android.view.View
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.currentLocale
import com.topjohnwu.superuser.Shell
import java.io.File
import java.io.FileNotFoundException
import java.lang.reflect.Array as JArray
val packageName: String get() = get<Context>().packageName
@@ -83,8 +105,129 @@ fun Context.rawResource(id: Int) = resources.openRawResource(id)
fun Context.readUri(uri: Uri) =
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
fun Context.getBitmap(id: Int): Bitmap {
var drawable = AppCompatResources.getDrawable(this, id)!!
if (drawable is BitmapDrawable)
return drawable.bitmap
if (SDK_INT >= 26 && drawable is AdaptiveIconDrawable) {
drawable = LayerDrawable(arrayOf(drawable.background, drawable.foreground))
}
val bitmap = Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
}
fun Intent.startActivity(context: Context) = context.startActivity(this)
fun Intent.startActivityWithRoot() {
val args = mutableListOf("am", "start", "--user", Const.USER_ID.toString())
val cmd = toCommand(args).joinToString(" ")
Shell.su(cmd).submit()
}
fun Intent.toCommand(args: MutableList<String> = mutableListOf()): MutableList<String> {
action?.also {
args.add("-a")
args.add(it)
}
component?.also {
args.add("-n")
args.add(it.flattenToString())
}
data?.also {
args.add("-d")
args.add(it.toString())
}
categories?.also {
for (cat in it) {
args.add("-c")
args.add(cat)
}
}
type?.also {
args.add("-t")
args.add(it)
}
extras?.also {
loop@ for (key in it.keySet()) {
val v = it[key] ?: continue
var value: Any = v
val arg: String
when {
v is String -> arg = "--es"
v is Boolean -> arg = "--ez"
v is Int -> arg = "--ei"
v is Long -> arg = "--el"
v is Float -> arg = "--ef"
v is Uri -> arg = "--eu"
v is ComponentName -> {
arg = "--ecn"
value = v.flattenToString()
}
v is List<*> -> {
if (v.isEmpty())
continue@loop
arg = if (v[0] is Int)
"--eial"
else if (v[0] is Long)
"--elal"
else if (v[0] is Float)
"--efal"
else if (v[0] is String)
"--esal"
else
continue@loop /* Unsupported */
val sb = StringBuilder()
for (o in v) {
sb.append(o.toString().replace(",", "\\,"))
sb.append(',')
}
// Remove trailing comma
sb.deleteCharAt(sb.length - 1)
value = sb
}
v.javaClass.isArray -> {
arg = if (v is IntArray)
"--eia"
else if (v is LongArray)
"--ela"
else if (v is FloatArray)
"--efa"
else if (v is Array<*> && v.isArrayOf<String>())
"--esa"
else
continue@loop /* Unsupported */
val sb = StringBuilder()
val len = JArray.getLength(v)
for (i in 0 until len) {
sb.append(JArray.get(v, i)!!.toString().replace(",", "\\,"))
sb.append(',')
}
// Remove trailing comma
sb.deleteCharAt(sb.length - 1)
value = sb
}
else -> continue@loop
} /* Unsupported */
args.add(arg)
args.add(key)
args.add(value.toString())
}
}
args.add("-f")
args.add(flags.toString())
return args
}
fun File.provide(context: Context = get()): Uri {
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
}
@@ -119,3 +262,50 @@ fun ApplicationInfo.getLabel(pm: PackageManager): String {
return loadLabel(pm).toString()
}
fun Intent.exists(packageManager: PackageManager) = resolveActivity(packageManager) != null
fun Context.colorCompat(@ColorRes id: Int) = try {
ContextCompat.getColor(this, id)
} catch (e: Resources.NotFoundException) {
null
}
fun Context.colorStateListCompat(@ColorRes id: Int) = try {
ContextCompat.getColorStateList(this, id)
} catch (e: Resources.NotFoundException) {
null
}
fun Context.drawableCompat(@DrawableRes id: Int) = ContextCompat.getDrawable(this, id)
/**
* Pass [start] and [end] dimensions, function will return left and right
* with respect to RTL layout direction
*/
fun Context.startEndToLeftRight(start: Int, end: Int): Pair<Int, Int> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
) {
return end to start
}
return start to end
}
fun Context.openUrl(url: String) = Utils.openLink(this, url.toUri())
@Suppress("FunctionName")
inline fun <reified T> T.DynamicClassLoader(apk: File)
= DynamicClassLoader(apk, T::class.java.classLoader)
fun Context.unwrap() : Context {
var context = this
while (true) {
if (context is ContextWrapper)
context = context.baseContext
else
break
}
return context
}
fun Uri.writeTo(file: File) = toFile().copyTo(file)

View File

@@ -1,6 +1,6 @@
package com.topjohnwu.magisk.extensions
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.utils.KObservableField
fun KObservableField<Boolean>.toggle() {

View File

@@ -1,11 +1,11 @@
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.lang.reflect.Field
import java.lang.reflect.Method
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -19,8 +19,6 @@ fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
}
}
fun Uri.writeTo(file: File) = toFile().copyTo(file)
fun InputStream.writeTo(file: File) =
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
@@ -101,3 +99,32 @@ fun Locale.toLangTag(): String {
return tag.toString()
}
}
// Reflection hacks
private val loadClass = ClassLoader::class.java.getMethod("loadClass", String::class.java)
private val getDeclaredMethod = Class::class.java.getMethod("getDeclaredMethod",
String::class.java, arrayOf<Class<*>>()::class.java)
private val getDeclaredField = Class::class.java.getMethod("getDeclaredField", String::class.java)
fun ClassLoader.forceLoadClass(name: String) =
runCatching { loadClass.invoke(this, name) }.getOrNull() as Class<*>?
fun Class<*>.forceGetDeclaredMethod(name: String, vararg types: Class<*>) =
(runCatching { getDeclaredMethod.invoke(this, name, types) }.getOrNull() as Method?)?.also {
it.isAccessible = true
}
fun Class<*>.forceGetDeclaredField(name: String) =
(runCatching { getDeclaredField.invoke(this, name) }.getOrNull() as Field?)?.also {
it.isAccessible = true
}
inline fun <reified T> T.forceGetClass(name: String) =
T::class.java.classLoader?.forceLoadClass(name)
fun Class<*>.forceGetField(name: String): Field? =
forceGetDeclaredField(name) ?: superclass?.forceGetField(name)
fun Class<*>.forceGetMethod(name: String, vararg types: Class<*>): Method? =
forceGetDeclaredMethod(name, *types) ?: superclass?.forceGetMethod(name, *types)

View File

@@ -2,8 +2,7 @@ package com.topjohnwu.magisk.extensions
import androidx.collection.SparseArrayCompat
import androidx.databinding.ObservableList
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import com.topjohnwu.magisk.utils.DiffObservableList
import io.reactivex.disposables.Disposable
fun <T> MutableList<T>.update(newList: List<T>) {

View File

@@ -1,9 +0,0 @@
package com.topjohnwu.magisk.extensions
import io.reactivex.Single
import io.reactivex.functions.BiFunction
fun <T : Any> T.toSingle() = Single.just(this)
fun <T1, T2, R> zip(t1: Single<T1>, t2: Single<T2>, zipper: (T1, T2) -> R) =
Single.zip(t1, t2, BiFunction<T1, T2, R> { rt1, rt2 -> zipper(rt1, rt2) })

View File

@@ -25,3 +25,5 @@ fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
.replace("#", "").replace("@", "").replace("\\", "_")
fun String.isEmptyInternal() = isNullOrBlank()

View File

@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.binding
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.model.entity.recycler.LenientRvItem
import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter

View File

@@ -1,22 +1,24 @@
package com.topjohnwu.magisk.model.download
import android.annotation.SuppressLint
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.core.app.NotificationCompat
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.extensions.chooser
import com.topjohnwu.magisk.extensions.exists
import com.topjohnwu.magisk.extensions.provide
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.APKInstall
import org.koin.core.get
import java.io.File
import kotlin.random.Random.Default.nextInt
@@ -61,50 +63,62 @@ open class DownloadService : RemoteFileService() {
remove(id)
when (subject.configuration) {
is APK.Upgrade -> APKInstall.install(this, subject.file)
else -> Unit
is APK.Restore -> Unit
}
}
// ---
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
override fun Notification.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)
private fun Notification.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))
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
fileIntent(subject.file)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
}
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
else -> this
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
private fun Notification.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))
Download -> this.apply {
fileIntent(subject.file.parentFile!!)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_parent, it.chooser()) }
fileIntent(subject.file)
.takeIf { it.exists(get()) }
?.let { addAction(0, R.string.download_open_self, it.chooser()) }
}
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
else -> this
}
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
private fun Notification.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) =
private fun Notification.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) =
private fun Notification.Builder.addAction(icon: Int, title: Int, intent: Intent) =
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
.let { addAction(icon, getString(title), it) }
@@ -115,7 +129,6 @@ open class DownloadService : RemoteFileService() {
.setDataAndType(file.provide(this), file.type)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
.chooser()
}
class Builder {
@@ -127,8 +140,7 @@ open class DownloadService : RemoteFileService() {
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)
val intent = app.intent<DownloadService>().putExtra(ARG_URL, builder.subject)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
app.startForegroundService(intent)

View File

@@ -1,23 +1,18 @@
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.*
import com.topjohnwu.magisk.extensions.writeTo
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) {
private fun RemoteFileService.patch(apk: File, id: Int) {
if (packageName == BuildConfig.APPLICATION_ID)
return
update(id) { notification ->
notification.setProgress(0, 0, true)
.setProgress(0, 0, true)
@@ -25,19 +20,28 @@ private fun RemoteFileService.patchPackage(apk: File, id: Int) {
.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)
}
PatchAPK.patch(apk, patched, packageName, applicationInfo.nonLocalizedLabel.toString())
apk.delete()
patched.renameTo(apk)
}
private fun RemoteFileService.upgrade(apk: File, id: Int) {
if (isRunningAsStub) {
// Move to upgrade location
apk.copyTo(DynAPK.update(this), overwrite = true)
apk.delete()
if (Info.stub!!.version < Info.remote.stub.versionCode) {
// We also want to upgrade stub
service.fetchFile(Info.remote.stub.link).blockingGet().byteStream().use {
it.writeTo(apk)
}
patch(apk, id)
} else {
// Simply relaunch the app
ProcessPhoenix.triggerRebirth(this, intent<ProcessPhoenix>())
}
} else {
patch(apk, id)
}
}
@@ -51,15 +55,11 @@ private fun RemoteFileService.restore(apk: File, id: Int) {
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)
}
Shell.su("pm install $apk && pm uninstall $packageName").exec()
}
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
= when (subject.configuration) {
is Upgrade -> patchPackage(subject.file, subject.hashCode())
is Upgrade -> upgrade(subject.file, subject.hashCode())
is Restore -> restore(subject.file, subject.hashCode())
}

View File

@@ -1,23 +1,22 @@
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 com.topjohnwu.magisk.base.BaseService
import com.topjohnwu.magisk.view.Notifications
import org.koin.core.KoinComponent
import java.util.*
import kotlin.random.Random.Default.nextInt
abstract class NotificationService : Service() {
abstract class NotificationService : BaseService(), KoinComponent {
abstract val defaultNotification: NotificationCompat.Builder
abstract val defaultNotification: Notification.Builder
private val manager by lazy { NotificationManagerCompat.from(this) }
private val hasNotifications get() = notifications.isNotEmpty()
private val notifications =
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
Collections.synchronizedMap(mutableMapOf<Int, Notification.Builder>())
override fun onTaskRemoved(rootIntent: Intent?) {
super.onTaskRemoved(rootIntent)
@@ -29,7 +28,7 @@ abstract class NotificationService : Service() {
fun update(
id: Int,
body: (NotificationCompat.Builder) -> Unit = {}
body: (Notification.Builder) -> Unit = {}
) {
val notification = notifications.getOrPut(id) { defaultNotification }
@@ -42,7 +41,7 @@ abstract class NotificationService : Service() {
protected fun finishNotify(
id: Int,
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
editBody: (Notification.Builder) -> Notification.Builder? = { null }
) : Int {
val currentNotification = remove(id)?.run(editBody)
@@ -61,11 +60,11 @@ abstract class NotificationService : Service() {
// ---
private fun notify(id: Int, notification: Notification) {
manager.notify(id, notification)
Notifications.mgr.notify(id, notification)
}
private fun cancel(id: Int) {
manager.cancel(id)
Notifications.mgr.cancel(id)
}
protected fun remove(id: Int) = notifications.remove(id).also {

View File

@@ -1,13 +1,13 @@
package com.topjohnwu.magisk.model.download
import android.app.Activity
import android.app.Notification
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.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
@@ -22,9 +22,9 @@ import java.io.InputStream
abstract class RemoteFileService : NotificationService() {
private val service: GithubRawServices by inject()
val service: GithubRawServices by inject()
override val defaultNotification: NotificationCompat.Builder
override val defaultNotification
get() = Notifications.progress(this, "")
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
@@ -108,8 +108,8 @@ abstract class RemoteFileService : NotificationService() {
@Throws(Throwable::class)
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
: NotificationCompat.Builder
protected abstract fun Notification.Builder.addActions(subject: DownloadSubject)
: Notification.Builder
companion object {
const val ARG_URL = "arg_url"

View File

@@ -1,10 +1,14 @@
package com.topjohnwu.magisk.model.entity
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.topjohnwu.magisk.extensions.now
import com.topjohnwu.magisk.extensions.timeFormatTime
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
import java.util.*
@Entity(tableName = "logs")
data class MagiskLog(
val fromUid: Int,
val toUid: Int,
@@ -13,9 +17,10 @@ data class MagiskLog(
val appName: String,
val command: String,
val action: Boolean,
val date: Date
val time: Long = -1
) {
val timeString = date.time.toTime(timeFormatTime)
@PrimaryKey(autoGenerate = true) var id: Int = 0
@Ignore val timeString = time.toTime(timeFormatTime)
}
data class WrappedMagiskLog(
@@ -23,35 +28,8 @@ data class WrappedMagiskLog(
val items: List<MagiskLog>
)
fun Map<String, String>.toLog(): MagiskLog {
return MagiskLog(
fromUid = get("from_uid")?.toIntOrNull() ?: -1,
toUid = get("to_uid")?.toIntOrNull() ?: -1,
fromPid = get("from_pid")?.toIntOrNull() ?: -1,
packageName = get("package_name").orEmpty(),
appName = get("app_name").orEmpty(),
command = get("command").orEmpty(),
action = get("action")?.toIntOrNull() != 0,
date = get("time")?.toLongOrNull()?.toDate() ?: Date()
)
}
fun Long.toDate() = Date(this)
fun MagiskLog.toMap() = mapOf(
"from_uid" to fromUid,
"to_uid" to toUid,
"from_pid" to fromPid,
"package_name" to packageName,
"app_name" to appName,
"command" to command,
"action" to action,
"time" to date.time
)
fun MagiskPolicy.toLog(
toUid: Int,
fromPid: Int,
command: String,
date: Date
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, date)
command: String
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, now)

View File

@@ -7,11 +7,11 @@ import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
data class MagiskPolicy(
val uid: Int,
var uid: Int,
val packageName: String,
val appName: String,
val policy: Int = INTERACTIVE,
val until: Long = -1L,
var policy: Int = INTERACTIVE,
var until: Long = -1L,
val logging: Boolean = true,
val notification: Boolean = true,
val applicationInfo: ApplicationInfo
@@ -38,7 +38,7 @@ fun MagiskPolicy.toMap() = mapOf(
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
val uid = get("uid")?.toIntOrNull() ?: -1
val packageName = get("package_name").orEmpty()
val info = pm.getApplicationInfo(packageName, 0)
val info = pm.getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
@@ -56,14 +56,15 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
}
@Throws(PackageManager.NameNotFoundException::class)
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
fun Int.toPolicy(pm: PackageManager, policy: Int = INTERACTIVE): MagiskPolicy {
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
?: throw PackageManager.NameNotFoundException()
val info = pm.getApplicationInfo(pkg, 0)
val info = pm.getApplicationInfo(pkg, PackageManager.GET_UNINSTALLED_PACKAGES)
return MagiskPolicy(
uid = this,
uid = info.uid,
packageName = pkg,
policy = policy,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
appName = info.getLabel(pm)
)
}

View File

@@ -8,7 +8,8 @@ import se.ansman.kotshi.JsonSerializable
data class UpdateInfo(
val app: ManagerJson = ManagerJson(),
val uninstaller: UninstallerJson = UninstallerJson(),
val magisk: MagiskJson = MagiskJson()
val magisk: MagiskJson = MagiskJson(),
val stub: StubJson = StubJson()
)
@JsonSerializable
@@ -33,3 +34,9 @@ data class ManagerJson(
val link: String = "",
val note: String = ""
) : Parcelable
@JsonSerializable
data class StubJson(
val versionCode: Int = -1,
val link: String = ""
)

View File

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

View File

@@ -13,17 +13,21 @@ class Module(path: String) : BaseModule() {
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")
private val removeFile = SuFile(path, "remove")
private val disableFile = SuFile(path, "disable")
private val updateFile = SuFile(path, "update")
private val ruleFile = SuFile(path, "sepolicy.rule")
val updated: Boolean = updateFile.exists()
var enable: Boolean = !disableFile.exists()
set(enable) {
val dir = "$PERSIST/$id"
field = if (enable) {
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
disableFile.delete()
} else {
Shell.su("rm -rf $dir").submit()
!disableFile.createNewFile()
}
}
@@ -31,8 +35,10 @@ class Module(path: String) : BaseModule() {
var remove: Boolean = removeFile.exists()
set(remove) {
field = if (remove) {
Shell.su("rm -rf $PERSIST/$id").submit()
removeFile.createNewFile()
} else {
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
!removeFile.delete()
}
}
@@ -48,12 +54,14 @@ class Module(path: String) : BaseModule() {
}
if (name.isEmpty()) {
name = id;
name = id
}
}
companion object {
private const val PERSIST = "/sbin/.magisk/mirror/persist/magisk"
@WorkerThread
fun loadModules(): List<Module> {
val moduleList = mutableListOf<Module>()
@@ -65,7 +73,7 @@ class Module(path: String) : BaseModule() {
val module = Module(Const.MAGISK_PATH + "/" + file.name)
moduleList.add(module)
}
return moduleList
return moduleList.sortedBy { it.name.toLowerCase() }
}
}
}

View File

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

View File

@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.RecyclerView
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.ComparableRvItem
/**
* This item addresses issues where enclosing recycler has to be invalidated or generally

View File

@@ -1,14 +1,14 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.timeFormatMedium
import com.topjohnwu.magisk.extensions.toTime
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
class LogRvItem : ComparableRvItem<LogRvItem>() {
override val layoutRes: Int = R.layout.item_page_log
@@ -37,8 +37,10 @@ class LogItemRvItem(
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemRvItem): Boolean = items
.any { !other.items.contains(it) }
override fun contentSameAs(other: LogItemRvItem): Boolean {
if (items.size != other.items.size) return false
return items.all { it in other.items }
}
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
}
@@ -50,13 +52,7 @@ class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvI
fun toggle() = isExpanded.toggle()
override fun contentSameAs(other: LogItemEntryRvItem) = item.fromUid == other.item.fromUid &&
item.toUid == other.item.toUid &&
item.fromPid == other.item.fromPid &&
item.packageName == other.item.packageName &&
item.command == other.item.command &&
item.action == other.item.action &&
item.date == other.item.date
override fun contentSameAs(other: LogItemEntryRvItem) = item == other.item
override fun itemSameAs(other: LogItemEntryRvItem) = item.appName == other.item.appName
}

View File

@@ -2,14 +2,14 @@ package com.topjohnwu.magisk.model.entity.recycler
import android.content.res.Resources
import androidx.annotation.StringRes
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
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
import com.topjohnwu.magisk.utils.KObservableField
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {

View File

@@ -1,16 +1,16 @@
package com.topjohnwu.magisk.model.entity.recycler
import android.graphics.drawable.Drawable
import com.skoumal.teanity.databinding.ComparableRvItem
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.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
class SectionRvItem(val text: String) : ComparableRvItem<SectionRvItem>() {
override val layoutRes: Int = R.layout.item_section

View File

@@ -1,7 +1,7 @@
package com.topjohnwu.magisk.model.entity.recycler
import com.skoumal.teanity.databinding.ComparableRvItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ComparableRvItem
class SpinnerRvItem(val item: String) : ComparableRvItem<SpinnerRvItem>() {

View File

@@ -0,0 +1,25 @@
package com.topjohnwu.magisk.model.events
internal interface EventHandler {
/**
* Called for all [ViewEvent]s published by associated viewModel.
* For [SimpleViewEvent]s, both this and [onSimpleEventDispatched]
* methods are called - you can choose the way how you handle them.
*/
fun onEventDispatched(event: ViewEvent) {}
/**
* Called for all [SimpleViewEvent]s published by associated viewModel.
* Both this and [onEventDispatched] methods are called - you can choose
* the way how you handle them.
*/
fun onSimpleEventDispatched(event: Int) {}
val viewEventObserver get() = ViewEventObserver {
onEventDispatched(it)
if (it is SimpleViewEvent) {
onSimpleEventDispatched(it.event)
}
}
}

View File

@@ -1,10 +1,10 @@
package com.topjohnwu.magisk.model.events
import com.skoumal.teanity.rxbus.RxBus
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.utils.RxBus
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event

View File

@@ -0,0 +1,5 @@
package com.topjohnwu.magisk.model.events
class SimpleViewEvent(
val event: Int
) : ViewEvent()

View File

@@ -0,0 +1,27 @@
package com.topjohnwu.magisk.model.events
import android.content.Context
import androidx.annotation.StringRes
import com.google.android.material.snackbar.Snackbar
class SnackbarEvent private constructor(
@StringRes private val messageRes: Int,
private val messageString: String?,
val length: Int,
val f: Snackbar.() -> Unit
) : ViewEvent() {
constructor(
@StringRes messageRes: Int,
length: Int = Snackbar.LENGTH_SHORT,
f: Snackbar.() -> Unit = {}
) : this(messageRes, null, length, f)
constructor(
message: String,
length: Int = Snackbar.LENGTH_SHORT,
f: Snackbar.() -> Unit = {}
) : this(-1, message, length, f)
fun message(context: Context): String = messageString ?: context.getString(messageRes)
}

View File

@@ -0,0 +1,17 @@
package com.topjohnwu.magisk.model.events
import androidx.lifecycle.Observer
/**
* Observer for [ViewEvent]s, which automatically checks if event was handled
*/
class ViewEventObserver(private val onEventUnhandled: (ViewEvent) -> Unit) : Observer<ViewEvent> {
override fun onChanged(event: ViewEvent?) {
event?.let {
if (!it.handled) {
it.handled = true
onEventUnhandled(it)
}
}
}
}

View File

@@ -1,10 +1,19 @@
package com.topjohnwu.magisk.model.events
import android.app.Activity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.model.entity.module.Repo
import io.reactivex.subjects.PublishSubject
/**
* Class for passing events from ViewModels to Activities/Fragments
* Variable [handled] used so each event is handled only once
* (see https://medium.com/google-developers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150)
* Use [ViewEventObserver] for observing these events
*/
abstract class ViewEvent {
var handled = false
}
data class OpenLinkEvent(val url: String) : ViewEvent()
@@ -19,7 +28,7 @@ class EnvFixEvent : ViewEvent()
class UpdateSafetyNetEvent : ViewEvent()
class ViewActionEvent(val action: Activity.() -> Unit) : ViewEvent()
class ViewActionEvent(val action: BaseActivity<*, *>.() -> Unit) : ViewEvent()
class OpenFilePickerEvent : ViewEvent()

View File

@@ -4,10 +4,12 @@ import android.os.Bundle
import androidx.annotation.AnimRes
import androidx.annotation.AnimatorRes
import androidx.fragment.app.Fragment
import com.skoumal.teanity.viewevents.NavigationDslMarker
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import kotlin.reflect.KClass
@DslMarker
annotation class NavigationDslMarker
class MagiskNavigationEvent(
val navDirections: MagiskNavDirectionsBuilder,
val navOptions: MagiskNavOptions,

View File

@@ -1,76 +1,46 @@
package com.topjohnwu.magisk.model.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.BaseReceiver
import com.topjohnwu.magisk.data.database.PolicyDao
import com.topjohnwu.magisk.data.database.base.su
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.SuLogger
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.utils.SuHandler
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import org.koin.core.inject
open class GeneralReceiver : BroadcastReceiver() {
open class GeneralReceiver : BaseReceiver() {
private val policyDB: PolicyDao by inject()
companion object {
const val REQUEST = "request"
const val LOG = "log"
const val NOTIFY = "notify"
const val TEST = "test"
}
private fun getPkg(intent: Intent): String {
return intent.data?.encodedSchemeSpecificPart.orEmpty()
}
override fun onReceive(context: Context, intent: Intent?) {
override fun onReceive(context: ContextWrapper, intent: Intent?) {
intent ?: return
when (intent.action ?: return) {
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
val action = intent.getStringExtra("action")
if (action == null) {
// Actual boot completed event
Shell.su("mm_patch_dtbo").submit {
if (it.isSuccess)
Notifications.dtboPatched(context)
Intent.ACTION_REBOOT -> {
SuHandler(context, intent.getStringExtra("action"), intent.extras)
}
return
}
when (action) {
REQUEST -> {
val i = Intent(context, ClassMap[SuRequestActivity::class.java])
.setAction(action)
.putExtra("socket", intent.getStringExtra("socket"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
context.startActivity(i)
}
LOG -> SuLogger.handleLogs(intent)
NOTIFY -> SuLogger.handleNotify(intent)
TEST -> Shell.su("magisk --use-broadcast").submit()
}
}
Intent.ACTION_PACKAGE_REPLACED ->
Intent.ACTION_PACKAGE_REPLACED -> {
// This will only work pre-O
if (Config.suReAuth)
policyDB.delete(getPkg(intent)).blockingGet()
}
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
policyDB.delete(pkg).blockingGet()
"magiskhide --rm $pkg".su().blockingGet()
Shell.su("magiskhide --rm $pkg").submit()
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
Const.Key.BROADCAST_MANAGER_UPDATE -> {

View File

@@ -3,9 +3,9 @@ package com.topjohnwu.magisk.model.update
import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.base.DelegateWorker
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.model.worker.DelegateWorker
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell
@@ -20,7 +20,7 @@ class UpdateCheckService : DelegateWorker() {
magiskRepo.fetchUpdate().blockingGet()
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
Notifications.managerUpdate(applicationContext)
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
else if (Info.env.magiskVersionCode < Info.remote.magisk.versionCode)
Notifications.magiskUpdate(applicationContext)
ListenableWorker.Result.success()
}.getOrElse {

View File

@@ -2,11 +2,11 @@ package com.topjohnwu.magisk.tasks
import android.content.Context
import android.net.Uri
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.extensions.fileName
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.readUri
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.utils.unzip
import com.topjohnwu.superuser.Shell
import io.reactivex.Single

View File

@@ -6,7 +6,6 @@ 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
@@ -25,9 +24,11 @@ import org.kamranzafar.jtar.TarHeader
import org.kamranzafar.jtar.TarInputStream
import org.kamranzafar.jtar.TarOutputStream
import timber.log.Timber
import java.io.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.nio.ByteBuffer
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
@@ -40,7 +41,7 @@ abstract class MagiskInstaller {
private val console: MutableList<String>
private val logs: MutableList<String>
private var isTar = false
private var tarOut: TarOutputStream? = null
private val service: GithubRawServices by inject()
private val context: Context by inject()
@@ -151,7 +152,9 @@ abstract class MagiskInstaller {
private fun handleTar(input: InputStream) {
console.add("- Processing tar file")
var vbmeta = false
withStreams(TarInputStream(input), TarOutputStream(destFile)) { tarIn, tarOut ->
val tarOut = TarOutputStream(destFile)
this.tarOut = tarOut
TarInputStream(input).use { tarIn ->
lateinit var entry: TarEntry
while (tarIn.nextEntry?.let { entry = it } != null) {
if (entry.name.contains("boot.img") || entry.name.contains("recovery.img")) {
@@ -215,8 +218,7 @@ abstract class MagiskInstaller {
return false
}
it.reset()
if (Arrays.equals(magic, "ustar".toByteArray())) {
isTar = true
if (magic.contentEquals("ustar".toByteArray())) {
destFile = File(Config.downloadDirectory, "magisk_patched.tar")
handleTar(it)
} else {
@@ -264,7 +266,7 @@ abstract class MagiskInstaller {
val patched = File(installDir, "new-boot.img")
if (isSigned) {
console.add("- Signing boot image with test keys")
console.add("- Signing boot image with verity keys")
val signed = File(installDir, "signed.img")
try {
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
@@ -285,23 +287,23 @@ abstract class MagiskInstaller {
protected fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false
if (!Info.keepVerity)
"patch_dtbo_image".sh()
arrayOf(
"(KEEPVERITY=${Info.keepVerity} patch_dtb_partitions)",
"run_migrations"
).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(
val os = tarOut?.let {
it.putNextEntry(newEntry(
if (srcBoot.contains("recovery")) "recovery.img" else "boot.img",
patched.length()))
} else {
os = destFile.outputStream()
}
tarOut = null
it
} ?: destFile.outputStream()
patched.suInputStream().use { it.copyTo(os); os.close() }
} catch (e: IOException) {
console.add("! Failed to output to $destFile")

View File

@@ -2,17 +2,27 @@ package com.topjohnwu.magisk.ui
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
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 androidx.fragment.app.FragmentTransaction
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.ActivityMainBinding
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.ui.hide.MagiskHideFragment
import com.topjohnwu.magisk.ui.home.HomeFragment
import com.topjohnwu.magisk.ui.log.LogFragment
@@ -21,17 +31,23 @@ 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.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel
import timber.log.Timber
import kotlin.reflect.KClass
open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
open class MainActivity : BaseActivity<MainViewModel, ActivityMainBinding>(), Navigator,
FragNavController.RootFragmentListener, FragNavController.TransactionListener {
override val layoutRes: Int = R.layout.activity_main
override val viewModel: MainViewModel by viewModel()
override val navHostId: Int = R.id.main_nav_host
override val defaultPosition: Int = 0
private val navHostId: Int = R.id.main_nav_host
private val defaultPosition: Int = 0
private val navigationController by lazy {
FragNavController(supportFragmentManager, navHostId)
}
private val isRootFragment get() =
navigationController.currentStackIndex != defaultPosition
override val baseFragments: List<KClass<out Fragment>> = listOf(
HomeFragment::class,
@@ -43,17 +59,29 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
SettingsFragment::class
)
/*override fun getDarkTheme(): Int {
return R.style.AppTheme_Dark
}*/
override fun onCreate(savedInstanceState: Bundle?) {
if (!SplashActivity.DONE) {
startActivity(Intent(this, ClassMap[SplashActivity::class.java]))
startActivity(intent<SplashActivity>())
finish()
}
super.onCreate(savedInstanceState)
if (Info.env.isUnsupported && !viewModel.shownUnsupportedDialog) {
viewModel.shownUnsupportedDialog = true
AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(getString(R.string.unsupport_magisk_msg, Const.Version.MIN_VERSION))
.setPositiveButton(android.R.string.ok, null)
.show()
}
navigationController.apply {
rootFragmentListener = this@MainActivity
transactionListener = this@MainActivity
initialize(defaultPosition, savedInstanceState)
}
checkHideSection()
setSupportActionBar(binding.mainInclude.mainToolbar)
@@ -68,6 +96,11 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigationController.onSaveInstanceState(outState)
}
override fun setTitle(title: CharSequence?) {
supportActionBar?.title = title
}
@@ -76,6 +109,78 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
supportActionBar?.setTitle(titleId)
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
binding.drawerLayout.closeDrawer(binding.navView)
} else {
val fragment = navigationController.currentFrag as? BaseFragment<*, *>
if (fragment?.onBackPressed() == true) {
return
}
try {
navigationController.popFragment()
} catch (e: UnsupportedOperationException) {
when {
isRootFragment -> {
val options = FragNavTransactionOptions.newBuilder()
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.build()
navigationController.switchTab(defaultPosition, options)
}
else -> super.onBackPressed()
}
}
}
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
is BackPressEvent -> onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(this)
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
override fun onSimpleEventDispatched(event: Int) {
super.onSimpleEventDispatched(event)
when (event) {
Navigation.Main.OPEN_NAV -> openNav()
}
}
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
private fun checkHideSection() {
val menu = binding.navView.menu
menu.findItem(R.id.magiskHideFragment).isVisible = Info.env.isActive && Info.env.magiskHide
menu.findItem(R.id.modulesFragment).isVisible = Info.env.isActive
menu.findItem(R.id.reposFragment).isVisible = Info.isConnected.value && Info.env.isActive
menu.findItem(R.id.logFragment).isVisible = Info.env.isActive
menu.findItem(R.id.superuserFragment).isVisible = Utils.showSuperUser()
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
}
override val numberOfRootFragments: Int get() = baseFragments.size
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
override fun onTabTransaction(fragment: Fragment?, index: Int) {
val fragmentId = when (fragment) {
is HomeFragment -> R.id.magiskFragment
@@ -90,34 +195,58 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
binding.navView.setCheckedItem(fragmentId)
}
override fun onBackPressed() {
if (binding.drawerLayout.isDrawerOpen(binding.navView)) {
binding.drawerLayout.closeDrawer(binding.navView)
} else {
super.onBackPressed()
override fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
navigationController.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
navigationController.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.let { if (it == -1) null else it } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { navigationController.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
override fun onSimpleEventDispatched(event: Int) {
super.onSimpleEventDispatched(event)
when (event) {
Navigation.Main.OPEN_NAV -> openNav()
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(this, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
-1 -> destination.newInstance()
.apply { arguments = event.navDirections.args }
.let { navigationController.pushFragment(it) }
// When it's desired that fragments of same class are put on top of one another edit this
else -> navigationController.switchTab(index)
}
}
private fun openNav() = binding.drawerLayout.openDrawer(GravityCompat.START)
private fun checkHideSection() {
val menu = binding.navView.menu
menu.findItem(R.id.magiskHideFragment).isVisible =
Shell.rootAccess() && Config.magiskHide
menu.findItem(R.id.modulesFragment).isVisible =
Shell.rootAccess() && Info.magiskVersionCode >= 0
menu.findItem(R.id.reposFragment).isVisible =
(viewModel.isConnected.value && Shell.rootAccess() && Info.magiskVersionCode >= 0)
menu.findItem(R.id.logFragment).isVisible =
Shell.rootAccess()
menu.findItem(R.id.superuserFragment).isVisible =
Utils.showSuperUser()
}
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) = Unit
}

View File

@@ -2,11 +2,13 @@ package com.topjohnwu.magisk.ui
import android.view.MenuItem
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.model.navigation.Navigation
import com.topjohnwu.magisk.ui.base.MagiskViewModel
class MainViewModel : MagiskViewModel() {
class MainViewModel : BaseViewModel() {
var shownUnsupportedDialog = false
fun navPressed() = Navigation.Main.OPEN_NAV.publish()

View File

@@ -1,33 +1,24 @@
package com.topjohnwu.magisk.ui
import android.content.Intent
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ShellUtils
open class SplashActivity : AppCompatActivity() {
open class SplashActivity : Activity() {
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base.wrap())
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Shell.getShell {
if (Info.magiskVersionCode > 0 && Info.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(R.string.unsupport_magisk_message)
.setNegativeButton(R.string.ok, null)
.setOnDismissListener { finish() }
.show()
} else {
initAndStart()
}
}
Shell.getShell { initAndStart() }
}
private fun initAndStart() {
@@ -36,7 +27,7 @@ open class SplashActivity : AppCompatActivity() {
Config.suManager = ""
Shell.su("pm uninstall $pkg").submit()
}
if (TextUtils.equals(pkg, packageName)) {
if (pkg == packageName) {
runCatching {
// We are the manager, remove com.topjohnwu.magisk as it could be malware
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
@@ -44,6 +35,10 @@ open class SplashActivity : AppCompatActivity() {
}
}
Info.keepVerity = ShellUtils.fastCmd("echo \$KEEPVERITY").toBoolean()
Info.keepEnc = ShellUtils.fastCmd("echo \$KEEPFORCEENCRYPT").toBoolean()
Info.recovery = ShellUtils.fastCmd("echo \$RECOVERYMODE").toBoolean()
// Set default configs
Config.initialize()
@@ -56,10 +51,18 @@ open class SplashActivity : AppCompatActivity() {
// Setup shortcuts
Shortcuts.setup(this)
val intent = Intent(this, ClassMap[MainActivity::class.java])
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
if (Info.isNewReboot) {
val shell = Shell.newInstance()
shell.newJob().add("mm_patch_dtb").submit {
if (it.isSuccess)
Notifications.dtboPatched(this)
shell.close()
}
}
DONE = true
startActivity(intent)
startActivity(intent<MainActivity>().apply { intent?.also { putExtras(it) } })
finish()
}

View File

@@ -1,62 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.preference.*
import androidx.recyclerview.widget.RecyclerView
import com.topjohnwu.magisk.R
import org.koin.android.ext.android.inject
abstract class BasePreferenceFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
protected val prefs: SharedPreferences by inject()
protected val activity get() = requireActivity() as MagiskActivity<*, *>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val v = super.onCreateView(inflater, container, savedInstanceState)
prefs.registerOnSharedPreferenceChangeListener(this)
return v
}
override fun onDestroyView() {
prefs.unregisterOnSharedPreferenceChangeListener(this)
super.onDestroyView()
}
override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> {
return object : PreferenceGroupAdapter(preferenceScreen) {
@SuppressLint("RestrictedApi")
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
when (val preference = getItem(position)) {
is PreferenceCategory -> setZeroPaddingToLayoutChildren(holder.itemView)
else -> holder.itemView.findViewById<View>(R.id.icon_frame)?.isVisible =
preference.icon != null
}
}
}
}
private fun setZeroPaddingToLayoutChildren(view: View) {
(view as? ViewGroup)?.children?.forEach {
setZeroPaddingToLayoutChildren(it)
} ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
view.setPaddingRelative(0, view.paddingTop, view.paddingEnd, view.paddingBottom)
else
view.setPadding(0, view.paddingTop, view.paddingRight, view.paddingBottom)
}
}

View File

@@ -1,242 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SparseArrayCompat
import androidx.core.net.toUri
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.ncapdevi.fragnav.FragNavController
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
import com.topjohnwu.magisk.model.navigation.MagiskAnimBuilder
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
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.currentLocale
import timber.log.Timber
import kotlin.reflect.KClass
typealias RequestCallback = MagiskActivity<*, *>.(Int, Intent?) -> Unit
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
Navigator, FragNavController.TransactionListener {
override val numberOfRootFragments: Int get() = baseFragments.size
override val baseFragments: List<KClass<out Fragment>> = listOf()
private val resultCallbacks = SparseArrayCompat<RequestCallback>()
protected open val defaultPosition: Int = 0
protected val navigationController get() = if (navHostId == 0) null else _navigationController
private val _navigationController by lazy {
if (navHostId == 0) throw IllegalStateException("Did you forget to override \"navHostId\"?")
FragNavController(supportFragmentManager, navHostId)
}
private val isRootFragment
get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false
init {
val theme = if (Config.darkTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(theme)
}
override fun applyOverrideConfiguration(config: Configuration?) {
// Force applying our preferred local
config?.setLocale(currentLocale)
super.applyOverrideConfiguration(config)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleManager.getLocaleContext(base))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigationController?.apply {
rootFragmentListener = this@MagiskActivity
transactionListener = this@MagiskActivity
initialize(defaultPosition, savedInstanceState)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
navigationController?.onSaveInstanceState(outState)
}
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(this)
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
override fun getRootFragment(index: Int) = baseFragments[index].java.newInstance()
override fun navigateTo(event: MagiskNavigationEvent) {
val directions = event.navDirections
navigationController?.defaultTransactionOptions = FragNavTransactionOptions.newBuilder()
.customAnimations(event.animOptions)
.build()
navigationController?.currentStack
?.indexOfFirst { it.javaClass == event.navOptions.popUpTo }
?.let { if (it == -1) null else it } // invalidate if class is not found
?.let { if (event.navOptions.inclusive) it + 1 else it }
?.let { navigationController?.popFragments(it) }
when (directions.isActivity) {
true -> navigateToActivity(event)
else -> navigateToFragment(event)
}
}
private fun navigateToActivity(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
val options = event.navOptions
Intent(this, destination)
.putExtras(event.navDirections.args)
.apply {
if (options.singleTop) addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
if (options.clearTask) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
}
.let { startActivity(it) }
}
private fun navigateToFragment(event: MagiskNavigationEvent) {
val destination = event.navDirections.destination?.java ?: let {
Timber.e("Cannot navigate to null destination")
return
}
when (val index = baseFragments.indexOfFirst { it.java.name == destination.name }) {
-1 -> destination.newInstance()
.apply { arguments = event.navDirections.args }
.let { navigationController?.pushFragment(it) }
// When it's desired that fragments of same class are put on top of one another edit this
else -> navigationController?.switchTab(index)
}
}
override fun onBackPressed() {
val fragment = navigationController?.currentFrag as? MagiskFragment<*, *>
if (fragment?.onBackPressed() == true) {
return
}
try {
navigationController?.popFragment() ?: throw UnsupportedOperationException()
} catch (e: UnsupportedOperationException) {
when {
isRootFragment -> {
val options = FragNavTransactionOptions.newBuilder()
.transition(FragmentTransaction.TRANSIT_FRAGMENT_CLOSE)
.build()
navigationController?.switchTab(defaultPosition, options)
}
else -> super.onBackPressed()
}
}
}
override fun onFragmentTransaction(
fragment: Fragment?,
transactionType: FragNavController.TransactionType
) = Unit
override fun onTabTransaction(fragment: Fragment?, index: Int) = Unit
fun openUrl(url: String) = Utils.openLink(this, url.toUri())
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build()
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
if (report.areAllPermissionsGranted()) {
request.onSuccess()
} else {
request.onFailure()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>,
token: PermissionToken
) = token.continuePermissionRequest()
}).check()
}
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
customAnimations(options.enter, options.exit, options.popEnter, options.popExit).apply {
if (!options.anySet) {
transition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.apply {
resultCallbacks.remove(requestCode)
invoke(this@MagiskActivity, resultCode, data)
}
}
fun startActivityForResult(
intent: Intent,
requestCode: Int,
listener: RequestCallback
) {
resultCallbacks[requestCode] = listener
startActivityForResult(intent, requestCode)
}
}

View File

@@ -1,52 +0,0 @@
package com.topjohnwu.magisk.ui.base
import androidx.annotation.CallSuper
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import com.skoumal.teanity.view.TeanityFragment
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.model.navigation.MagiskNavigationEvent
import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import kotlin.reflect.KClass
abstract class MagiskFragment<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityFragment<ViewModel, Binding>(), Navigator {
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) = activity.navigateTo(event)
@CallSuper
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is BackPressEvent -> activity.onBackPressed()
is MagiskNavigationEvent -> navigateTo(event)
is ViewActionEvent -> event.action(requireActivity())
is PermissionEvent -> activity.withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
fun withPermissions(vararg permissions: String, builder: PermissionRequestBuilder.() -> Unit) {
activity.withPermissions(*permissions, builder = builder)
}
fun openLink(url: String) = activity.openUrl(url)
open fun onBackPressed(): Boolean = false
}

View File

@@ -2,22 +2,29 @@ package com.topjohnwu.magisk.ui.flash
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
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.base.BaseActivity
import com.topjohnwu.magisk.databinding.ActivityFlashBinding
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.extensions.snackbar
import com.topjohnwu.magisk.intent
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.SnackbarEvent
import com.topjohnwu.magisk.model.events.ViewEvent
import com.topjohnwu.magisk.view.Notifications
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koin.core.parameter.parametersOf
import java.io.File
open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>() {
open class FlashActivity : BaseActivity<FlashViewModel, ActivityFlashBinding>() {
override val layoutRes: Int = R.layout.activity_flash
override val themeRes: Int = R.style.MagiskTheme_Flashing
override val viewModel: FlashViewModel by viewModel {
val uri = intent.data ?: let { finish(); Uri.EMPTY }
val additionalUri = intent.getParcelableExtra(Const.Key.FLASH_DATA) ?: uri
@@ -26,10 +33,11 @@ open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>(
}
override fun onCreate(savedInstanceState: Bundle?) {
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_NOSENSOR
super.onCreate(savedInstanceState)
val id = intent.getIntExtra(Const.Key.DISMISS_ID, -1)
if (id != -1)
NotificationManagerCompat.from(this).cancel(id)
Notifications.mgr.cancel(id)
}
override fun onBackPressed() {
@@ -37,9 +45,24 @@ open class FlashActivity : MagiskActivity<FlashViewModel, ActivityFlashBinding>(
super.onBackPressed()
}
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is SnackbarEvent -> snackbar(snackbarView, event.message(this), event.length, event.f)
is BackPressEvent -> onBackPressed()
is PermissionEvent -> withPermissions(*event.permissions.toTypedArray()) {
onSuccess { event.callback.onNext(true) }
onFailure {
event.callback.onNext(false)
event.callback.onError(SecurityException("User refused permissions"))
}
}
}
}
companion object {
private fun intent(context: Context) = Intent(context, ClassMap[FlashActivity::class.java])
private fun intent(context: Context) = context.intent<FlashActivity>()
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
private fun intent(context: Context, file: File) = intent(context).setData(file.toUri())

View File

@@ -7,21 +7,20 @@ import android.net.Uri
import android.os.Handler
import androidx.core.os.postDelayed
import androidx.databinding.ObservableArrayList
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.subscribeK
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.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.events.SnackbarEvent
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.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.superuser.Shell
import me.tatarka.bindingcollectionadapter2.ItemBinding
import java.io.File
@@ -32,7 +31,7 @@ class FlashViewModel(
installer: Uri,
uri: Uri,
private val resources: Resources
) : MagiskViewModel(), FlashResultListener {
) : BaseViewModel(), FlashResultListener {
val canShowReboot = Shell.rootAccess()
val showRestartTitle = KObservableField(false)

View File

@@ -1,28 +1,28 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.extensions.addOnPropertyChangedCallback
import com.topjohnwu.magisk.extensions.subscribeK
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.DiffObservableList
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.RxBus
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
class HideViewModel(
private val magiskRepo: MagiskRepository,
rxBus: RxBus
) : MagiskViewModel() {
) : BaseViewModel() {
val query = KObservableField("")
val isShowSystem = KObservableField(false)
@@ -90,9 +90,7 @@ class HideViewModel(
.toList()
.map { it to items.calculateDiff(it) }
private fun toggleItem(item: HideProcessRvItem) = magiskRepo
.toggleHide(item.isHidden.value, item.packageName, item.process)
.subscribeK()
.add()
private fun toggleItem(item: HideProcessRvItem) =
magiskRepo.toggleHide(item.isHidden.value, item.packageName, item.process)
}

View File

@@ -6,11 +6,11 @@ import android.view.MenuItem
import android.widget.SearchView
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentMagiskHideBinding
import com.topjohnwu.magisk.ui.base.MagiskFragment
import org.koin.androidx.viewmodel.ext.android.viewModel
class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBinding>(),
class MagiskHideFragment : BaseFragment<HideViewModel, FragmentMagiskHideBinding>(),
SearchView.OnQueryTextListener {
override val layoutRes: Int = R.layout.fragment_magisk_hide

View File

@@ -1,31 +1,31 @@
package com.topjohnwu.magisk.ui.home
import android.content.Context
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseActivity
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
import com.topjohnwu.magisk.extensions.inject
import com.topjohnwu.magisk.extensions.DynamicClassLoader
import com.topjohnwu.magisk.extensions.openUrl
import com.topjohnwu.magisk.extensions.subscribeK
import com.topjohnwu.magisk.extensions.writeTo
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.utils.DynamicClassLoader
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.superuser.Shell
import dalvik.system.DexFile
import io.reactivex.Completable
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
import java.lang.reflect.InvocationHandler
class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
class HomeFragment : BaseFragment<HomeViewModel, FragmentMagiskBinding>(),
SafetyNetHelper.Callback {
override val layoutRes: Int = R.layout.fragment_magisk
@@ -33,13 +33,14 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
private val magiskRepo: MagiskRepository by inject()
private val EXT_APK by lazy { File("${activity.filesDir.parent}/snet", "snet.jar") }
private val EXT_DEX by lazy { File(EXT_APK.parent, "snet.dex") }
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
override fun onEventDispatched(event: ViewEvent) {
super.onEventDispatched(event)
when (event) {
is OpenLinkEvent -> openLink(event.url)
is OpenLinkEvent -> activity.openUrl(event.url)
is ManagerInstallEvent -> installManager()
is MagiskInstallEvent -> installMagisk()
is UninstallEvent -> uninstall()
@@ -62,7 +63,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
return
}
MagiskInstallDialog(requireActivity() as MagiskActivity<*, *>).show()
MagiskInstallDialog(requireActivity() as BaseActivity<*, *>).show()
}
private fun installManager() = ManagerInstallDialog(requireActivity()).show()
@@ -86,15 +87,15 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
.setTitle(R.string.proprietary_title)
.setMessage(R.string.proprietary_notice)
.setCancelable(false)
.setPositiveButton(R.string.yes) { _, _ -> download() }
.setNegativeButton(R.string.no_thanks) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
.setPositiveButton(android.R.string.yes) { _, _ -> download() }
.setNegativeButton(android.R.string.no) { _, _ -> viewModel.finishSafetyNetCheck(-2) }
.show()
}
private fun updateSafetyNet(dieOnError: Boolean) {
Completable.fromAction {
val loader = DynamicClassLoader(EXT_APK)
val dex = DexFile.loadDex(EXT_APK.path, EXT_APK.parent, 0)
val dex = DexFile.loadDex(EXT_APK.path, EXT_DEX.path, 0)
// Scan through the dex and find our helper class
var helperClass: Class<*>? = null

View File

@@ -1,28 +1,23 @@
package com.topjohnwu.magisk.ui.home
import android.content.pm.PackageManager
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSubscribeUi
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.base.viewmodel.BaseViewModel
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.extensions.get
import com.topjohnwu.magisk.extensions.packageName
import com.topjohnwu.magisk.extensions.res
import com.topjohnwu.magisk.extensions.toggle
import com.topjohnwu.magisk.extensions.*
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.KObservableField
import com.topjohnwu.magisk.utils.SafetyNetHelper
import com.topjohnwu.superuser.Shell
import io.reactivex.Completable
enum class SafetyNetState {
LOADING, PASS, FAILED, IDLE
}
enum class MagiskState {
NO_ROOT, NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
NOT_INSTALLED, UP_TO_DATE, OBSOLETE, LOADING
}
enum class MagiskItem {
@@ -31,7 +26,7 @@ enum class MagiskItem {
class HomeViewModel(
private val magiskRepo: MagiskRepository
) : MagiskViewModel(State.LOADED) {
) : BaseViewModel(State.LOADED) {
val hasGMS = runCatching {
get<PackageManager>().getPackageInfo("com.google.android.gms", 0); true
@@ -43,13 +38,9 @@ class HomeViewModel(
val isKeepVerity = KObservableField(Info.keepVerity)
val isRecovery = KObservableField(Info.recovery)
private val _magiskState = KObservableField(MagiskState.LOADING)
val magiskState = Observer(_magiskState, isConnected) {
if (isConnected.value) _magiskState.value else MagiskState.UP_TO_DATE
}
val magiskState = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) {
when (magiskState.value) {
MagiskState.NO_ROOT -> TODO()
MagiskState.NOT_INSTALLED -> R.string.magisk_version_error.res()
MagiskState.UP_TO_DATE -> R.string.magisk_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
@@ -71,7 +62,6 @@ class HomeViewModel(
}
val managerStateText = Observer(managerState) {
when (managerState.value) {
MagiskState.NO_ROOT -> "wtf"
MagiskState.NOT_INSTALLED -> R.string.invalid_update_channel.res()
MagiskState.UP_TO_DATE -> R.string.manager_up_to_date.res()
MagiskState.LOADING -> R.string.checking_for_updates.res()
@@ -87,7 +77,7 @@ class HomeViewModel(
""
}
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text)
val safetyNetTitle = KObservableField(R.string.safetyNet_check_text.res())
val ctsState = KObservableField(SafetyNetState.IDLE)
val basicIntegrityState = KObservableField(SafetyNetState.IDLE)
val safetyNetState = Observer(ctsState, basicIntegrityState) {
@@ -102,7 +92,7 @@ class HomeViewModel(
}
}
val hasRoot = KObservableField(false)
val isActive = KObservableField(false)
private var shownDialog = false
@@ -117,10 +107,10 @@ class HomeViewModel(
Info.recovery = it ?: return@addOnPropertyChangedCallback
}
isConnected.addOnPropertyChangedCallback {
if (it == true) refresh()
if (it == true) refresh(false)
}
refresh()
refresh(false)
}
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
@@ -145,7 +135,7 @@ class HomeViewModel(
fun safetyNetPressed() {
ctsState.value = SafetyNetState.LOADING
basicIntegrityState.value = SafetyNetState.LOADING
safetyNetTitle.value = R.string.checking_safetyNet_status
safetyNetTitle.value = R.string.checking_safetyNet_status.res()
UpdateSafetyNetEvent().publish()
}
@@ -154,7 +144,7 @@ class HomeViewModel(
response and 0x0F == 0 -> {
val hasCtsPassed = response and SafetyNetHelper.CTS_PASS != 0
val hasBasicIntegrityPassed = response and SafetyNetHelper.BASIC_PASS != 0
safetyNetTitle.value = R.string.safetyNet_check_success
safetyNetTitle.value = R.string.safetyNet_check_success.res()
ctsState.value = if (hasCtsPassed) {
SafetyNetState.PASS
} else {
@@ -174,67 +164,83 @@ class HomeViewModel(
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = when (response) {
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid
else -> R.string.safetyNet_api_error
SafetyNetHelper.RESPONSE_ERR -> R.string.safetyNet_res_invalid.res()
else -> R.string.safetyNet_api_error.res()
}
}
}
fun refresh() {
refreshVersions()
@JvmOverloads
fun refresh(invalidate: Boolean = true) {
if (invalidate)
Info.envRef.invalidate()
magiskRepo.fetchUpdate()
isActive.value = Info.env.isActive
val fetchUpdate = if (isConnected.value)
magiskRepo.fetchUpdate().ignoreElement()
else
Completable.complete()
Completable.fromAction {
// Ensure value is ready
Info.env
}.andThen(fetchUpdate)
.applyViewModel(this)
.doOnSubscribeUi {
_magiskState.value = MagiskState.LOADING
magiskState.value = MagiskState.LOADING
_managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text
}
.subscribeK {
safetyNetTitle.value = R.string.safetyNet_check_text.res()
}.subscribeK {
updateSelf()
ensureEnv()
refreshVersions()
}
hasRoot.value = Shell.rootAccess()
}
private fun refreshVersions() {
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
version.format(Info.magiskVersionString, Info.magiskVersionCode)
VERSION_FMT.format(Info.env.magiskVersionString, Info.env.magiskVersionCode)
} else {
""
}
managerCurrentVersion.value = version
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
managerCurrentVersion.value = if (isRunningAsStub) MGR_VER_FMT
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, Info.stub!!.version)
else
VERSION_FMT.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
}
private fun updateSelf() {
_magiskState.value = when (Info.magiskVersionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
!in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
magiskState.value = when (Info.env.magiskVersionCode) {
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED
in 1 until Info.remote.magisk.versionCode -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
magiskLatestVersion.value = version
.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
magiskLatestVersion.value =
VERSION_FMT.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
_managerState.value = when (Info.remote.app.versionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
in Int.MIN_VALUE .. 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1) .. Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> {
if (Info.stub?.version ?: Int.MAX_VALUE < Info.remote.stub.versionCode)
MagiskState.OBSOLETE
else
MagiskState.UP_TO_DATE
}
}
managerLatestVersion.value = version
.format(Info.remote.app.version, Info.remote.app.versionCode)
managerLatestVersion.value = MGR_VER_FMT
.format(Info.remote.app.version, Info.remote.app.versionCode, Info.remote.stub.versionCode)
}
private fun ensureEnv() {
val invalidStates =
listOf(MagiskState.NOT_INSTALLED, MagiskState.NO_ROOT, MagiskState.LOADING)
listOf(MagiskState.NOT_INSTALLED, MagiskState.LOADING)
// Don't bother checking env when magisk is not installed, loading or already has been shown
if (invalidStates.any { it == magiskState.value } || shownDialog) return
@@ -246,7 +252,8 @@ class HomeViewModel(
}
companion object {
private const val version = "%s (%d)"
private const val VERSION_FMT = "%s (%d)"
private const val MGR_VER_FMT = "%s (%d) (%d)"
}
}

View File

@@ -6,14 +6,14 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.base.BaseFragment
import com.topjohnwu.magisk.databinding.FragmentLogBinding
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.model.events.ViewEvent
import org.koin.androidx.viewmodel.ext.android.viewModel
class LogFragment : MagiskFragment<LogViewModel, FragmentLogBinding>() {
class LogFragment : BaseFragment<LogViewModel, FragmentLogBinding>() {
override val layoutRes: Int = R.layout.fragment_log
override val viewModel: LogViewModel by viewModel()

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