Compare commits

..

186 Commits

Author SHA1 Message Date
topjohnwu
b6144ae582 Add v21.2 release notes 2020-12-28 15:35:09 -08:00
Arbri çoçka
afe17c73b4 Update strings.xml
Fix same text in Values-sq
2020-12-28 15:29:27 -08:00
topjohnwu
b51b884fc7 Fix module installs in recovery
Close #3494
2020-12-28 00:25:01 -08:00
topjohnwu
d3e4b29e62 Update README.md 2020-12-27 22:36:03 -08:00
dark-basic
24059e7403 Update Stub-es version 2020-12-27 22:09:03 -08:00
dark-basic
107a2a6682 Update String-es 2020-12-27 22:08:34 -08:00
Arbri çoçka
56b4ab6672 Fix any text in strings sq 2020-12-27 22:07:44 -08:00
topjohnwu
4662454938 More attempts to fix gradle cache on Windows 2020-12-27 20:13:50 -08:00
topjohnwu
db4f78d463 Unblock signals before executing commands 2020-12-27 15:05:39 -08:00
topjohnwu
880de21596 Update Github Actions
Disable gradle daemon on Windows and always upload artifacts
2020-12-27 04:02:20 -08:00
topjohnwu
622dd84c9e Fix uninstaller zip 2020-12-26 22:45:05 -08:00
topjohnwu
f983bfc883 Embed keys into dex files 2020-12-26 21:33:30 -08:00
topjohnwu
45cdb3fdb0 Update dependencies 2020-12-26 17:05:12 -08:00
topjohnwu
9a707236b8 Move signing code into main app sources 2020-12-26 17:03:10 -08:00
topjohnwu
e9e6ad3bb0 Sign zips with apksigner 2020-12-26 16:04:41 -08:00
topjohnwu
ab78a81d15 Fix GitHub actions 2020-12-25 15:54:47 -08:00
John Wu
18340099b7 Add GitHub actions
Enable GitHub actions to run CI on all 3 platforms
2020-12-25 15:01:02 -08:00
topjohnwu
a013696a41 Default to config.prop in buildSrc 2020-12-25 13:03:25 -08:00
topjohnwu
8a2a6d9232 Make versionCode unconfigurable 2020-12-25 05:34:15 -08:00
topjohnwu
12aa6d86e4 Make config.prop optional 2020-12-24 04:46:31 -08:00
topjohnwu
7d08969d28 Fix strings 2020-12-23 01:33:46 -08:00
Fs00
dda4aa8488 Translate missing Italian strings 2020-12-22 23:33:20 -08:00
binarynoise
cdaef3d801 Update install.md
I wanted to share my experiences with rooting my S10e.
2020-12-22 23:32:53 -08:00
amninder singh
9159166128 Update strings.xml
Updating strings.xml
regarding #3566 adding punjabi translation
2020-12-22 23:32:01 -08:00
Arbri çoçka
dc0882e043 fixing some errors in sq strings 2020-12-22 23:31:40 -08:00
amninder singh
c811f015ef Added Punjabi Translation
- Written in Gurmukhi Script containing different foreign punctuations both inscript and Phonetic
2020-12-22 23:31:12 -08:00
nkh0472
d8f0b66fe1 Improve Correctness and Clarity of guides.md 2020-12-22 23:30:37 -08:00
Mohd Faraz
dc3d57deba utils_functions: Added a check for the system_root
now on addon while flashing recovery usign mount point /system_root by which this is causing a flashing error.
Let's first check and unmount /system_root if mounted

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

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

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

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

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

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

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

Because:

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

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

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

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

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

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

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

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

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

@@ -0,0 +1,87 @@
name: Magisk Build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
name: Build on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macOS-latest ]
steps:
- name: Check out
uses: actions/checkout@v2
with:
submodules: 'recursive'
fetch-depth: 0
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: '11'
- name: Set up Python 3
uses: actions/setup-python@v2
with:
python-version: '3.x'
- name: Set up GitHub env (Windows)
if: runner.os == 'Windows'
run: |
$oldAndroidPath = $env:ANDROID_SDK_ROOT
$sdk_root = "C:\Android"
New-Item -Path $sdk_root -ItemType SymbolicLink -Value $oldAndroidPath
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
echo "ANDROID_SDK_ROOT=$sdk_root" >> $env:GITHUB_ENV
echo "ANDROID_HOME=$sdk_root" >> $env:GITHUB_ENV
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $env:GITHUB_ENV
- name: Set up GitHub env (Unix)
if: runner.os != 'Windows'
run: |
ndk_ver=$(sed -n 's/^magisk.fullNdkVersion=//p' gradle.properties)
echo ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT >> $GITHUB_ENV
echo MAGISK_NDK_VERSION=$ndk_ver >> $GITHUB_ENV
- name: Cache Gradle
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
!~/.gradle/caches/**/*.lock
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Cache NDK
id: ndk-cache
uses: actions/cache@v2
with:
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }}
- name: Set up NDK
if: steps.ndk-cache.outputs.cache-hit != 'true'
run: python build.py ndk
- name: Build release
run: python build.py -vr all
- name: Build debug
run: python build.py -v all
# Only upload artifacts built on Linux
- name: Upload build artifact
if: runner.os == 'Linux'
uses: actions/upload-artifact@v2
with:
name: ${{ github.sha }}
path: out

View File

@@ -15,11 +15,11 @@ Here are some feature highlights:
## Downloads ## Downloads
[![](https://img.shields.io/badge/Magisk%20Manager-v8.0.1-green)](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.1/MagiskManager-v8.0.1.apk) [![](https://img.shields.io/badge/Magisk%20Manager-v8.0.3-green)](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.3/MagiskManager-v8.0.3.apk)
[![](https://img.shields.io/badge/Magisk%20Manager-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk) [![](https://img.shields.io/badge/Magisk%20Manager-Canary-red)](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
<br> <br>
[![](https://img.shields.io/badge/Magisk-v20.4-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v20.4) [![](https://img.shields.io/badge/Magisk-v20.4-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
[![](https://img.shields.io/badge/Magisk%20Beta-v21.0-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v21.0) [![](https://img.shields.io/badge/Magisk%20Beta-v21.1-blue)](https://github.com/topjohnwu/Magisk/releases/tag/v21.1)
## Useful Links ## Useful Links
@@ -56,13 +56,13 @@ For Magisk Manager crashes, record and upload the logcat when the crash occurs.
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"` - macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"`
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"` - Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH` - Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
- Set environment variable `ANDROID_HOME` to the Android SDK folder (can be found in Android Studio settings) - Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
- Run `./build.py ndk` to let the script download and install NDK for you - Run `./build.py ndk` to let the script download and install NDK for you
- Set configurations in `config.prop`. A sample `config.prop.sample` is provided.
- To start building, run `build.py` to see your options. \ - To start building, run `build.py` to see your options. \
For each action, use `-h` to access help (e.g. `./build.py all -h`) For each action, use `-h` to access help (e.g. `./build.py all -h`)
- To start development, open the project in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building. - To start development, open the project in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
- `build.py` builds in debug mode by default. If you want release builds (with `-r, --release`), you need a Java Keystore to sign APKs and zips. For more information, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key). - Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
## Translation Contributions ## Translation Contributions

View File

@@ -1,7 +1,9 @@
import java.io.PrintStream
plugins { plugins {
id("com.android.application") id("com.android.application")
kotlin("android") kotlin("android")
kotlin("android.extensions") kotlin("plugin.parcelize")
kotlin("kapt") kotlin("kapt")
id("androidx.navigation.safeargs.kotlin") id("androidx.navigation.safeargs.kotlin")
} }
@@ -20,12 +22,11 @@ android {
applicationId = "com.topjohnwu.magisk" applicationId = "com.topjohnwu.magisk"
vectorDrawables.useSupportLibrary = true vectorDrawables.useSupportLibrary = true
multiDexEnabled = true multiDexEnabled = true
versionName = Config["appVersion"] versionName = Config.appVersion
versionCode = Config["appVersionCode"]?.toInt() versionCode = Config.appVersionCode
buildConfigField("int", "LATEST_MAGISK", Config["versionCode"] ?: "Integer.MAX_VALUE")
javaCompileOptions.annotationProcessorOptions.arguments( javaCompileOptions.annotationProcessorOptions.arguments(
mapOf("room.incremental" to "true") mapOf("room.incremental" to "true")
) )
} }
@@ -51,11 +52,12 @@ android {
packagingOptions { packagingOptions {
exclude("/META-INF/**") exclude("/META-INF/**")
exclude("/androidsupportmultidexversion.txt")
exclude("/org/bouncycastle/**") exclude("/org/bouncycastle/**")
exclude("/kotlin/**") exclude("/kotlin/**")
exclude("/kotlinx/**") exclude("/kotlinx/**")
exclude("/okhttp3/**") exclude("/okhttp3/**")
exclude("/*.txt")
exclude("/*.bin")
} }
kotlinOptions { kotlinOptions {
@@ -63,28 +65,61 @@ android {
} }
} }
androidExtensions { tasks["preBuild"]?.dependsOn(tasks.register("copyUtils", Copy::class) {
isExperimental = true
}
val copyUtils = tasks.register("copyUtils", Copy::class) {
from(rootProject.file("scripts/util_functions.sh")) from(rootProject.file("scripts/util_functions.sh"))
into("src/main/res/raw") into("src/main/res/raw")
} })
tasks["preBuild"]?.dependsOn(copyUtils) android.applicationVariants.all {
val keysDir = rootProject.file("tools/keys")
val outSrcDir = File(buildDir, "generated/source/keydata/$name")
val outSrc = File(outSrcDir, "com/topjohnwu/signing/KeyData.java")
fun PrintStream.newField(name: String, file: File) {
println("public static byte[] $name() {")
print("byte[] buf = {")
val bytes = file.readBytes()
print(bytes.joinToString(",") { "(byte)(${it.toInt() and 0xff})" })
println("};")
println("return buf;")
println("}")
}
val genSrcTask = tasks.register("generate${name.capitalize()}KeyData") {
inputs.dir(keysDir)
outputs.file(outSrc)
doLast {
outSrc.parentFile.mkdirs()
PrintStream(outSrc).use {
it.println("package com.topjohnwu.signing;")
it.println("public final class KeyData {")
it.newField("testCert", File(keysDir, "testkey.x509.pem"))
it.newField("testKey", File(keysDir, "testkey.pk8"))
it.newField("verityCert", File(keysDir, "verity.x509.pem"))
it.newField("verityKey", File(keysDir, "verity.pk8"))
it.println("}")
}
}
}
registerJavaGeneratingTask(genSrcTask.get(), outSrcDir)
}
dependencies { dependencies {
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
implementation(kotlin("stdlib")) implementation(kotlin("stdlib"))
implementation(project(":app:shared")) implementation(project(":app:shared"))
implementation(project(":app:signing"))
implementation("com.github.topjohnwu:jtar:1.0.0") implementation("com.github.topjohnwu:jtar:1.0.0")
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7") implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
implementation("com.github.topjohnwu:lz4-java:1.7.1") implementation("com.github.topjohnwu:lz4-java:1.7.1")
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")
val vBC = "1.68"
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}")
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}")
val vBAdapt = "4.0.0" val vBAdapt = "4.0.0"
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter" val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
implementation("${bindingAdapter}:${vBAdapt}") implementation("${bindingAdapter}:${vBAdapt}")
@@ -123,7 +158,7 @@ dependencies {
implementation("com.squareup.moshi:moshi:${vMoshi}") implementation("com.squareup.moshi:moshi:${vMoshi}")
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}") kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
val vRoom = "2.2.5" val vRoom = "2.3.0-alpha04"
implementation("androidx.room:room-runtime:${vRoom}") implementation("androidx.room:room-runtime:${vRoom}")
implementation("androidx.room:room-ktx:${vRoom}") implementation("androidx.room:room-ktx:${vRoom}")
kapt("androidx.room:room-compiler:${vRoom}") kapt("androidx.room:room-compiler:${vRoom}")
@@ -133,9 +168,9 @@ dependencies {
implementation("androidx.navigation:navigation-ui-ktx:${vNav}") implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
implementation("androidx.biometric:biometric:1.0.1") implementation("androidx.biometric:biometric:1.0.1")
implementation("androidx.constraintlayout:constraintlayout:2.0.1") implementation("androidx.constraintlayout:constraintlayout:2.0.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.browser:browser:1.2.0") implementation("androidx.browser:browser:1.3.0")
implementation("androidx.preference:preference:1.1.1") implementation("androidx.preference:preference:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.1.0") implementation("androidx.recyclerview:recyclerview:1.1.0")
implementation("androidx.fragment:fragment-ktx:1.2.5") implementation("androidx.fragment:fragment-ktx:1.2.5")

View File

@@ -35,12 +35,6 @@
void onResponse(org.json.JSONObject); void onResponse(org.json.JSONObject);
} }
# Fragments
# TODO: Remove when AGP 4.1 release
# https://issuetracker.google.com/issues/142601969
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
-keepnames class androidx.navigation.fragment.NavHostFragment
# Strip Timber verbose and debug logging # Strip Timber verbose and debug logging
-assumenosideeffects class timber.log.Timber.Tree { -assumenosideeffects class timber.log.Timber.Tree {
public void v(**); public void v(**);

View File

@@ -12,12 +12,6 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar" android:theme="@android:style/Theme.Translucent.NoTitleBar"
tools:ignore="UnusedAttribute"> tools:ignore="UnusedAttribute">
<provider
android:name="a.p"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
</provider>
</application> </application>
</manifest> </manifest>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,16 +4,15 @@
package="com.topjohnwu.magisk"> package="com.topjohnwu.magisk">
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> android:maxSdkVersion="29" />
<application <application
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:name="a.e" android:name="a.e"
android:allowBackup="true" android:allowBackup="false"
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning"> tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
<!-- Splash --> <!-- Splash -->
@@ -65,6 +64,15 @@
<!-- DownloadService --> <!-- DownloadService -->
<service android:name="a.j" /> <service android:name="a.j" />
<!-- FileProvider -->
<provider
android:name="a.p"
android:authorities="${applicationId}.provider"
android:directBootAware="true"
android:exported="false"
android:grantUriPermissions="true">
</provider>
<!-- Hardcode GMS version --> <!-- Hardcode GMS version -->
<meta-data <meta-data
android:name="com.google.android.gms.version" android:name="com.google.android.gms.version"

View File

@@ -2,15 +2,16 @@
package a package a
import com.topjohnwu.magisk.core.App import com.topjohnwu.magisk.core.App
import com.topjohnwu.magisk.core.GeneralReceiver import com.topjohnwu.magisk.core.Provider
import com.topjohnwu.magisk.core.Receiver
import com.topjohnwu.magisk.core.SplashActivity import com.topjohnwu.magisk.core.SplashActivity
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.signing.BootSigner import com.topjohnwu.signing.SignBoot
fun main(args: Array<String>) { fun main(args: Array<String>) {
BootSigner.main(args) SignBoot.main(args)
} }
class b : MainActivity() class b : MainActivity()
@@ -22,8 +23,10 @@ class e : App {
constructor(o: Any) : super(o) constructor(o: Any) : super(o)
} }
class h : GeneralReceiver() class h : Receiver()
class j : DownloadService() class j : DownloadService()
class m : SuRequestActivity() class m : SuRequestActivity()
class p : Provider()

View File

@@ -1,5 +1,8 @@
package com.topjohnwu.magisk.arch package com.topjohnwu.magisk.arch
import android.content.res.Resources
import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
@@ -14,6 +17,7 @@ import androidx.navigation.fragment.NavHostFragment
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory
import com.topjohnwu.magisk.ui.theme.Theme import com.topjohnwu.magisk.ui.theme.Theme
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> : abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
@@ -41,6 +45,8 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
setTheme(themeRes) setTheme(themeRes)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -59,6 +65,31 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
directionsDispatcher.value = null directionsDispatcher.value = null
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window?.decorView?.let {
it.systemUiVisibility = (it.systemUiVisibility
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
window?.decorView?.post {
// If navigation bar is short enough (gesture navigation enabled), make it transparent
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) {
window.navigationBarColor = Color.TRANSPARENT
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.navigationBarDividerColor = Color.TRANSPARENT
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
window.isNavigationBarContrastEnforced = false
window.isStatusBarContrastEnforced = false
}
}
}
}
}
} }
fun setContentView() { fun setContentView() {
@@ -66,8 +97,10 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
it.setVariable(BR.viewModel, viewModel) it.setVariable(BR.viewModel, viewModel)
it.lifecycleOwner = this it.lifecycleOwner = this
} }
}
ensureInsets() fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
viewRoot.rootView.accessibilityDelegate = delegate
} }
override fun onResume() { override fun onResume() {
@@ -79,7 +112,7 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event) return currentFragment?.onKeyEvent(event) == true || super.dispatchKeyEvent(event)
} }
override fun onEventDispatched(event: ViewEvent) = when(event) { override fun onEventDispatched(event: ViewEvent) = when (event) {
is ContextExecutor -> event(this) is ContextExecutor -> event(this)
is ActivityExecutor -> event(this) is ActivityExecutor -> event(this)
else -> Unit else -> Unit

View File

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

View File

@@ -24,8 +24,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
override val viewRoot: View get() = binding.root override val viewRoot: View get() = binding.root
private val navigation get() = activity.navigation private val navigation get() = activity.navigation
override fun consumeSystemWindowInsets(insets: Insets) = insets
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
startObserveEvents() startObserveEvents()
@@ -65,7 +63,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
return true return true
} }
}) })
ensureInsets()
} }
override fun onResume() { override fun onResume() {

View File

@@ -9,18 +9,18 @@ import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding import me.tatarka.bindingcollectionadapter2.ItemBinding
import me.tatarka.bindingcollectionadapter2.OnItemBind import me.tatarka.bindingcollectionadapter2.OnItemBind
inline fun <T : ComparableRvItem<*>> diffListOf( fun <T : ComparableRvItem<*>> diffListOf(
vararg newItems: T vararg newItems: T
) = diffListOf(newItems.toList()) ) = diffListOf(newItems.toList())
inline fun <T : ComparableRvItem<*>> diffListOf( fun <T : ComparableRvItem<*>> diffListOf(
newItems: List<T> newItems: List<T>
) = DiffObservableList(object : DiffObservableList.Callback<T> { ) = DiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem) override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem) override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
}).also { it.update(newItems) } }).also { it.update(newItems) }
inline fun <T : ComparableRvItem<*>> filterableListOf( fun <T : ComparableRvItem<*>> filterableListOf(
vararg newItems: T vararg newItems: T
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> { ) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem) override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)

View File

@@ -10,8 +10,6 @@ import androidx.multidex.MultiDex
import androidx.work.WorkManager import androidx.work.WorkManager
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.FileProvider
import com.topjohnwu.magisk.core.su.SuCallbackHandler
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
import com.topjohnwu.magisk.core.utils.RootInit import com.topjohnwu.magisk.core.utils.RootInit
import com.topjohnwu.magisk.core.utils.updateConfig import com.topjohnwu.magisk.core.utils.updateConfig
@@ -36,7 +34,6 @@ open class App() : Application() {
.setInitializers(RootInit::class.java) .setInitializers(RootInit::class.java)
.setTimeout(2)) .setTimeout(2))
Shell.EXECUTOR = IODispatcherExecutor() Shell.EXECUTOR = IODispatcherExecutor()
FileProvider.callHandler = SuCallbackHandler
// Always log full stack trace with Timber // Always log full stack trace with Timber
Timber.plant(Timber.DebugTree()) Timber.plant(Timber.DebugTree())

View File

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

View File

@@ -16,9 +16,6 @@ object Const {
const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880" const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
// Misc // Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml"
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
val USER_ID = Process.myUid() / 100000 val USER_ID = Process.myUid() / 100000
object Version { object Version {
@@ -46,7 +43,6 @@ object Const {
} }
object Url { object Url {
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
const val PATREON_URL = "https://www.patreon.com/topjohnwu" const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk" const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
@@ -54,14 +50,13 @@ object Const {
const val GITHUB_API_URL = "https://api.github.com/" const val GITHUB_API_URL = "https://api.github.com/"
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/" const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/" const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
} }
object Key { object Key {
// others
const val LINK_KEY = "Link"
const val ETAG_KEY = "ETag"
// intents // intents
const val OPEN_SECTION = "section" const val OPEN_SECTION = "section"
const val PREV_PKG = "prev_pkg"
} }
object Value { object Value {

View File

@@ -149,7 +149,7 @@ private object ClassMap {
App::class.java to a.e::class.java, App::class.java to a.e::class.java,
MainActivity::class.java to a.b::class.java, MainActivity::class.java to a.b::class.java,
SplashActivity::class.java to a.c::class.java, SplashActivity::class.java to a.c::class.java,
GeneralReceiver::class.java to a.h::class.java, Receiver::class.java to a.h::class.java,
DownloadService::class.java to a.j::class.java, DownloadService::class.java to a.j::class.java,
SuRequestActivity::class.java to a.m::class.java SuRequestActivity::class.java to a.m::class.java
) )

View File

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

View File

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

View File

@@ -3,9 +3,9 @@ package com.topjohnwu.magisk.core
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.network.GithubRawServices import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
@@ -29,18 +29,18 @@ open class SplashActivity : Activity() {
} }
} }
private fun handleRepackage() { private fun handleRepackage(pkg: String?) {
val pkg = Config.suManager if (packageName != APPLICATION_ID) {
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
Config.suManager = ""
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
}
if (pkg == packageName) {
runCatching { runCatching {
// We are the manager, remove com.topjohnwu.magisk as it could be malware // Hidden, remove com.topjohnwu.magisk if exist as it could be malware
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0) packageManager.getApplicationInfo(APPLICATION_ID, 0)
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec() Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
} }
} else {
if (Config.suManager.isNotEmpty())
Config.suManager = ""
pkg ?: return
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
} }
} }
@@ -48,14 +48,16 @@ open class SplashActivity : Activity() {
// Pre-initialize root shell // Pre-initialize root shell
Shell.getShell() Shell.getShell()
Config.initialize() val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
handleRepackage()
Config.load(prevPkg)
handleRepackage(prevPkg)
Notifications.setup(this) Notifications.setup(this)
UpdateCheckService.schedule(this) UpdateCheckService.schedule(this)
Shortcuts.setupDynamic(this) Shortcuts.setupDynamic(this)
// Pre-fetch network stuffs // Pre-fetch network services
get<GithubRawServices>() get<NetworkService>()
DONE = true DONE = true

View File

@@ -1,6 +1,6 @@
package com.topjohnwu.magisk.core.base package com.topjohnwu.magisk.core.base
import android.Manifest import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -40,8 +40,8 @@ abstract class BaseActivity : AppCompatActivity() {
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) { fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
val request = PermissionRequestBuilder().apply(builder).build() val request = PermissionRequestBuilder().apply(builder).build()
if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 29) { if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
// We do not need external rw on 29+ // We do not need external rw on 30+
request.onSuccess() request.onSuccess()
return return
} }
@@ -52,7 +52,7 @@ abstract class BaseActivity : AppCompatActivity() {
var requestCode: Int var requestCode: Int
do { do {
requestCode = Random.nextInt(Const.ID.MAX_ACTIVITY_RESULT + 1, 1 shl 15) requestCode = Random.nextInt(Const.ID.MAX_ACTIVITY_RESULT + 1, 1 shl 15)
} while (!resultCallbacks.containsKey(requestCode)) } while (resultCallbacks.containsKey(requestCode))
resultCallbacks[requestCode] = { result, _ -> resultCallbacks[requestCode] = { result, _ ->
if (result > 0) if (result > 0)
request.onSuccess() request.onSuccess()
@@ -64,7 +64,7 @@ abstract class BaseActivity : AppCompatActivity() {
} }
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) { fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder) withPermission(WRITE_EXTERNAL_STORAGE, builder = builder)
} }
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(

View File

@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.download
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
sealed class Action : Parcelable { sealed class Action : Parcelable {
@@ -16,15 +16,6 @@ sealed class Action : Parcelable {
} }
sealed class APK : Action() {
@Parcelize
object Upgrade : APK()
@Parcelize
object Restore : APK()
}
@Parcelize @Parcelize
object Download : Action() object Download : Action()

View File

@@ -117,7 +117,7 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
fun Subject.notifyID() = hashCode() fun Subject.notifyID() = hashCode()
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) { private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
broadcast(-1f, subject) broadcast(-2f, subject)
it.setContentText(getString(R.string.download_file_error)) it.setContentText(getString(R.string.download_file_error))
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.setOngoing(false) .setOngoing(false)

View File

@@ -73,10 +73,7 @@ open class DownloadService : BaseDownloader() {
} }
private fun Notification.Builder.setIntent(subject: Manager) private fun Notification.Builder.setIntent(subject: Manager)
= when (subject.action) { = setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
else -> setContentIntent(Intent())
}
private fun Notification.Builder.setContentIntent(intent: Intent) = private fun Notification.Builder.setContentIntent(intent: Intent) =
setContentIntent( setContentIntent(

View File

@@ -5,20 +5,16 @@ import androidx.core.net.toFile
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.DynAPK import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action.APK.Restore
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.PatchAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.ktx.relaunchApp import com.topjohnwu.magisk.ktx.relaunchApp
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.superuser.Shell
import java.io.File import java.io.File
private fun Context.patch(apk: File) { private fun Context.patch(apk: File) {
val patched = File(apk.parent, "patched.apk") val patched = File(apk.parent, "patched.apk")
PatchAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel) HideAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
apk.delete() apk.delete()
patched.renameTo(apk) patched.renameTo(apk)
} }
@@ -31,7 +27,7 @@ private fun BaseDownloader.notifyHide(id: Int) {
} }
} }
private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) { suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) {
val apk = subject.file.toFile() val apk = subject.file.toFile()
val id = subject.notifyID() val id = subject.notifyID()
if (isRunningAsStub) { if (isRunningAsStub) {
@@ -53,20 +49,3 @@ private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
patch(apk) patch(apk)
} }
} }
private fun BaseDownloader.restore(apk: File, id: Int) {
update(id) {
it.setProgress(0, 0, true)
.setProgress(0, 0, true)
.setContentTitle(getString(R.string.restore_img_msg))
.setContentText("")
}
Config.export()
Shell.su("pm install $apk && pm uninstall $packageName").exec()
}
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) =
when (subject.action) {
is Upgrade -> upgrade(subject)
is Restore -> restore(subject.file.toFile(), subject.notifyID())
}

View File

@@ -8,12 +8,12 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.MagiskJson import com.topjohnwu.magisk.core.model.MagiskJson
import com.topjohnwu.magisk.core.model.ManagerJson import com.topjohnwu.magisk.core.model.ManagerJson
import com.topjohnwu.magisk.core.model.StubJson import com.topjohnwu.magisk.core.model.StubJson
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.ktx.cachedFile import com.topjohnwu.magisk.ktx.cachedFile
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.get
import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri() private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
@@ -26,10 +26,10 @@ sealed class Subject : Parcelable {
@Parcelize @Parcelize
class Module( class Module(
val module: Repo, val module: OnlineModule,
override val action: Action override val action: Action
) : Subject() { ) : Subject() {
override val url: String get() = module.zipUrl override val url: String get() = module.zip_url
override val title: String get() = module.downloadFilename override val title: String get() = module.downloadFilename
@IgnoredOnParcel @IgnoredOnParcel
@@ -40,16 +40,12 @@ sealed class Subject : Parcelable {
@Parcelize @Parcelize
class Manager( class Manager(
override val action: Action.APK,
private val app: ManagerJson = Info.remote.app, private val app: ManagerJson = Info.remote.app,
val stub: StubJson = Info.remote.stub val stub: StubJson = Info.remote.stub
) : Subject() { ) : Subject() {
override val action get() = Action.Download
override val title: String override val title: String get() = "MagiskManager-${app.version}(${app.versionCode})"
get() = "MagiskManager-${app.version}(${app.versionCode})" override val url: String get() = app.link
override val url: String
get() = app.link
@IgnoredOnParcel @IgnoredOnParcel
override val file by lazy { override val file by lazy {
@@ -105,7 +101,6 @@ sealed class Subject : Parcelable {
Action.Download -> Download() Action.Download -> Download()
Action.Uninstall -> Uninstall() Action.Uninstall -> Uninstall()
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config) Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
else -> throw IllegalArgumentException()
} }
} }

View File

@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.model
import android.os.Parcelable import android.os.Parcelable
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import kotlinx.android.parcel.Parcelize import kotlinx.parcelize.Parcelize
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class UpdateInfo( data class UpdateInfo(
@@ -41,3 +41,29 @@ data class StubJson(
val versionCode: Int = -1, val versionCode: Int = -1,
val link: String = "" val link: String = ""
) : Parcelable ) : Parcelable
@JsonClass(generateAdapter = true)
data class ModuleJson(
val id: String,
val last_update: Long,
val prop_url: String,
val zip_url: String,
val notes_url: String
)
@JsonClass(generateAdapter = true)
data class RepoJson(
val name: String,
val last_update: Long,
val modules: List<ModuleJson>
)
@JsonClass(generateAdapter = true)
data class CommitInfo(
val sha: String
)
@JsonClass(generateAdapter = true)
data class BranchInfo(
val commit: CommitInfo
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,20 @@
package com.topjohnwu.magisk.core.tasks package com.topjohnwu.magisk.core.tasks
import android.app.ProgressDialog
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION.SDK_INT
import android.widget.Toast import android.widget.Toast
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
import com.topjohnwu.magisk.DynAPK
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.*
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.utils.AXML import com.topjohnwu.magisk.core.utils.AXML
import com.topjohnwu.magisk.core.utils.Keygen import com.topjohnwu.magisk.core.utils.Keygen
import com.topjohnwu.magisk.data.repository.NetworkService import com.topjohnwu.magisk.data.repository.NetworkService
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.inject
import com.topjohnwu.magisk.ktx.writeTo import com.topjohnwu.magisk.ktx.writeTo
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.signing.JarMap import com.topjohnwu.signing.JarMap
import com.topjohnwu.signing.SignApk import com.topjohnwu.signing.SignApk
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
@@ -28,25 +28,28 @@ import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.security.SecureRandom import java.security.SecureRandom
object PatchAPK { object HideAPK {
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz" private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
private const val ALPHADOTS = "$ALPHA....." private const val ALPHADOTS = "$ALPHA....."
private const val APP_ID = "com.topjohnwu.magisk"
private const val APP_NAME = "Magisk Manager" private const val APP_NAME = "Magisk Manager"
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
// Some arbitrary limit // Some arbitrary limit
const val MAX_LABEL_LENGTH = 32 const val MAX_LABEL_LENGTH = 32
private fun genPackageName(): CharSequence { private val svc: NetworkService by inject()
private val Context.APK_URI get() = Provider.APK_URI(packageName)
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
private fun genPackageName(): String {
val random = SecureRandom() val random = SecureRandom()
val len = 5 + random.nextInt(15) val len = 5 + random.nextInt(15)
val builder = StringBuilder(len) val builder = StringBuilder(len)
var next: Char var next: Char
var prev = 0.toChar() var prev = 0.toChar()
for (i in 0 until len) { for (i in 0 until len) {
next = if (prev == '.' || i == len - 1) { next = if (prev == '.' || prev == 0.toChar() || i == len - 1) {
ALPHA[random.nextInt(ALPHA.length)] ALPHA[random.nextInt(ALPHA.length)]
} else { } else {
ALPHADOTS[random.nextInt(ALPHADOTS.length)] ALPHADOTS[random.nextInt(ALPHADOTS.length)]
@@ -59,20 +62,20 @@ object PatchAPK {
val idx = random.nextInt(len - 1) val idx = random.nextInt(len - 1)
builder[idx] = '.' builder[idx] = '.'
} }
return builder return builder.toString()
} }
fun patch( fun patch(
context: Context, context: Context,
apk: String, out: String, apk: String, out: String,
pkg: CharSequence, label: CharSequence pkg: String, label: CharSequence
): Boolean { ): Boolean {
try { try {
val jar = JarMap.open(apk) val jar = JarMap.open(apk)
val je = jar.getJarEntry(Const.ANDROID_MANIFEST) val je = jar.getJarEntry(ANDROID_MANIFEST)
val xml = AXML(jar.getRawData(je)) val xml = AXML(jar.getRawData(je))
if (!xml.findAndPatch(APP_ID to pkg.toString(), APP_NAME to label.toString())) if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
return false return false
// Write apk changes // Write apk changes
@@ -91,7 +94,6 @@ object PatchAPK {
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2() val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
val src = if (dlStub) { val src = if (dlStub) {
val stub = File(context.cacheDir, "stub.apk") val stub = File(context.cacheDir, "stub.apk")
val svc = get<NetworkService>()
try { try {
svc.fetchFile(Info.remote.stub.link).byteStream().use { svc.fetchFile(Info.remote.stub.link).byteStream().use {
it.writeTo(stub) it.writeTo(stub)
@@ -117,23 +119,74 @@ object PatchAPK {
if (!Shell.su("adb_pm_install $repack").exec().isSuccess) if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
return false return false
Config.suManager = pkg.toString() context.apply {
Config.export() val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return false
Shell.su("pm uninstall $APP_ID").submit() Config.suManager = pkg
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.putExtra(Const.Key.PREV_PKG, packageName)
startActivity(intent)
}
return true return true
} }
fun hideManager(context: Context, label: String) { @Suppress("DEPRECATION")
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title)) fun hide(context: Context, label: String) {
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build()) val dialog = ProgressDialog.show(context, context.getString(R.string.hide_manager_title), "", true)
GlobalScope.launch { GlobalScope.launch {
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
patchAndHide(context, label) patchAndHide(context, label)
} }
if (!result) if (!result) {
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG) Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID) dialog.dismiss()
}
}
}
private suspend fun downloadAndRestore(context: Context): Boolean {
val apk = if (isRunningAsStub) {
DynAPK.current(context)
} else {
File(context.cacheDir, "manager.apk").also { apk ->
try {
svc.fetchFile(Info.remote.app.link).byteStream().use {
it.writeTo(apk)
}
} catch (e: IOException) {
Timber.e(e)
return false
}
}
}
if (!Shell.su("adb_pm_install $apk").exec().isSuccess)
return false
context.apply {
val intent = packageManager.getLaunchIntentForPackage(APPLICATION_ID) ?: return false
Config.suManager = ""
grantUriPermission(APPLICATION_ID, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
grantUriPermission(APPLICATION_ID, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.putExtra(Const.Key.PREV_PKG, packageName)
startActivity(intent)
}
return true
}
@Suppress("DEPRECATION")
fun restore(context: Context) {
val dialog = ProgressDialog.show(context, context.getString(R.string.restore_img_msg), "", true)
GlobalScope.launch {
val result = withContext(Dispatchers.IO) {
downloadAndRestore(context)
}
if (!result) {
Utils.toast(R.string.restore_manager_fail_toast, Toast.LENGTH_LONG)
dialog.dismiss()
}
} }
} }
} }

View File

@@ -8,8 +8,10 @@ import android.widget.Toast
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
@@ -40,6 +42,8 @@ import org.koin.core.inject
import timber.log.Timber import timber.log.Timber
import java.io.* import java.io.*
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.security.SecureRandom
import java.util.*
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@@ -107,6 +111,8 @@ abstract class MagiskInstallImpl : KoinComponent {
} }
console.add("- Device platform: " + Build.CPU_ABI) console.add("- Device platform: " + Build.CPU_ABI)
console.add("- Magisk Manager: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
console.add("- Install target: ${Info.remote.magisk.version} (${Info.remote.magisk.versionCode})")
try { try {
ZipInputStream(zipUri.inputStream().buffered()).use { zi -> ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
@@ -225,7 +231,7 @@ abstract class MagiskInstallImpl : KoinComponent {
private fun handleFile(uri: Uri): Boolean { private fun handleFile(uri: Uri): Boolean {
val outStream: OutputStream val outStream: OutputStream
val outFile: MediaStoreUtils.UriFile var outFile: MediaStoreUtils.UriFile? = null
// Process input file // Process input file
try { try {
@@ -237,27 +243,40 @@ abstract class MagiskInstallImpl : KoinComponent {
return false return false
} }
src.reset() src.reset()
val alpha = "abcdefghijklmnopqrstuvwxyz"
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
val random = SecureRandom()
val suffix = StringBuilder()
for (i in 1..5) {
suffix.append(alphaNum[random.nextInt(alphaNum.length)])
}
val filename = "magisk_patched_$suffix"
outStream = if (magic.contentEquals("ustar".toByteArray())) { outStream = if (magic.contentEquals("ustar".toByteArray())) {
outFile = MediaStoreUtils.getFile("magisk_patched.tar") outFile = MediaStoreUtils.getFile("$filename.tar", true)
handleTar(src, outFile.uri.outputStream()) handleTar(src, outFile!!.uri.outputStream())
} else { } else {
// Raw image // Raw image
srcBoot = File(installDir, "boot.img").path srcBoot = File(installDir, "boot.img").path
console.add("- Copying image to cache") console.add("- Copying image to cache")
FileOutputStream(srcBoot).use { src.copyTo(it) } FileOutputStream(srcBoot).use { src.copyTo(it) }
outFile = MediaStoreUtils.getFile("magisk_patched.img") outFile = MediaStoreUtils.getFile("$filename.img", true)
outFile.uri.outputStream() outFile!!.uri.outputStream()
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
console.add("! Process error") console.add("! Process error")
outFile?.delete()
Timber.e(e) Timber.e(e)
return false return false
} }
// Patch file // Patch file
if (!patchBoot()) if (!patchBoot()) {
outFile!!.delete()
return false return false
}
// Output file // Output file
try { try {
@@ -276,6 +295,7 @@ abstract class MagiskInstallImpl : KoinComponent {
console.add("****************************") console.add("****************************")
} catch (e: IOException) { } catch (e: IOException) {
console.add("! Failed to output to $outFile") console.add("! Failed to output to $outFile")
outFile!!.delete()
Timber.e(e) Timber.e(e)
return false return false
} }
@@ -290,7 +310,7 @@ abstract class MagiskInstallImpl : KoinComponent {
srcBoot = File(installDir, "boot.img").path srcBoot = File(installDir, "boot.img").path
} }
var isSigned = false var isSigned: Boolean
try { try {
SuFileInputStream(srcBoot).use { SuFileInputStream(srcBoot).use {
isSigned = SignBoot.verifySignature(it, null) isSigned = SignBoot.verifySignature(it, null)
@@ -325,7 +345,7 @@ abstract class MagiskInstallImpl : KoinComponent {
val signed = File(installDir, "signed.img") val signed = File(installDir, "signed.img")
try { try {
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) { withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
input, out -> SignBoot.doSignature("/boot", input, out, null, null) input, out -> SignBoot.doSignature(null, null, input, out, "/boot")
} }
} catch (e: IOException) { } catch (e: IOException) {
console.add("! Unable to sign image") console.add("! Unable to sign image")
@@ -339,6 +359,13 @@ abstract class MagiskInstallImpl : KoinComponent {
return true return true
} }
private fun copySepolicyRules(): Boolean {
if (Info.remote.magisk.versionCode >= 21100) return true
// Copy existing rules for migration
"copy_sepolicy_rules".sh()
return true
}
private fun flashBoot(): Boolean { private fun flashBoot(): Boolean {
if (!"direct_install $installDir $srcBoot".sh().isSuccess) if (!"direct_install $installDir $srcBoot".sh().isSuccess)
return false return false
@@ -373,10 +400,11 @@ abstract class MagiskInstallImpl : KoinComponent {
protected fun doPatchFile(patchFile: Uri) = extractZip() && handleFile(patchFile) protected fun doPatchFile(patchFile: Uri) = extractZip() && handleFile(patchFile)
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot() protected fun direct() = findImage() && extractZip() && patchBoot() &&
copySepolicyRules() && flashBoot()
protected suspend fun secondSlot() = protected suspend fun secondSlot() = findSecondaryImage() && extractZip() &&
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA() patchBoot() && copySepolicyRules() && flashBoot() && postOTA()
protected fun fixEnv(zip: Uri): Boolean { protected fun fixEnv(zip: Uri): Boolean {
installDir = SuFile("/data/adb/magisk") installDir = SuFile("/data/adb/magisk")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,20 +1,18 @@
package com.topjohnwu.magisk.data.network package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.model.BranchInfo
import com.topjohnwu.magisk.core.model.RepoJson
import com.topjohnwu.magisk.core.model.UpdateInfo import com.topjohnwu.magisk.core.model.UpdateInfo
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.* import retrofit2.http.*
private const val REVISION = "revision" private const val REVISION = "revision"
private const val MODULE = "module" private const val BRANCH = "branch"
private const val FILE = "file" private const val REPO = "repo"
private const val IF_NONE_MATCH = "If-None-Match"
private const val MAGISK_FILES = "topjohnwu/magisk_files" const val MAGISK_FILES = "topjohnwu/magisk_files"
private const val MAGISK_MASTER = "topjohnwu/Magisk/master" const val MAGISK_MAIN = "topjohnwu/Magisk"
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
interface GithubPageServices { interface GithubPageServices {
@@ -34,27 +32,23 @@ interface JSDelivrServices {
@GET("$MAGISK_FILES@{$REVISION}/bootctl") @GET("$MAGISK_FILES@{$REVISION}/bootctl")
@Streaming @Streaming
suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody
@GET("$MAGISK_FILES@{$REVISION}/canary.json")
suspend fun fetchCanaryUpdate(@Path(REVISION) revision: String): UpdateInfo
@GET("$MAGISK_MAIN@{$REVISION}/scripts/module_installer.sh")
@Streaming
suspend fun fetchInstaller(@Path(REVISION) revision: String): ResponseBody
} }
interface GithubRawServices { interface RawServices {
@GET("$MAGISK_FILES/canary/debug.json")
suspend fun fetchCanaryUpdate(): UpdateInfo
@GET @GET
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
@GET("$MAGISK_MASTER/scripts/module_installer.sh") @GET
@Streaming suspend fun fetchRepoInfo(@Url url: String): RepoJson
suspend fun fetchInstaller(): ResponseBody
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
suspend fun fetchModuleFile(@Path(MODULE) id: String, @Path(FILE) file: String): String
/**
* This method shall be used exclusively for fetching files from urls from previous requests.
* Him, who uses it in a wrong way, shall die in an eternal flame.
* */
@GET @GET
@Streaming @Streaming
suspend fun fetchFile(@Url url: String): ResponseBody suspend fun fetchFile(@Url url: String): ResponseBody
@@ -66,13 +60,11 @@ interface GithubRawServices {
interface GithubApiServices { interface GithubApiServices {
@GET("users/${MAGISK_MODULES}/repos") @GET("repos/{$REPO}/branches/{$BRANCH}")
@Headers("Accept: application/vnd.github.v3+json") @Headers("Accept: application/vnd.github.v3+json")
suspend fun fetchRepos( suspend fun fetchBranch(
@Query("page") page: Int, @Path(REPO, encoded = true) repo: String,
@Header(IF_NONE_MATCH) etag: String, @Path(BRANCH) branch: String
@Query("sort") sort: String = "pushed", ): BranchInfo
@Query("per_page") count: Int = 100
): Response<List<GithubRepoInfo>>
} }

View File

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

View File

@@ -6,23 +6,21 @@ import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.*
import com.topjohnwu.magisk.data.network.GithubApiServices import com.topjohnwu.magisk.data.network.*
import com.topjohnwu.magisk.data.network.GithubPageServices
import com.topjohnwu.magisk.data.network.GithubRawServices
import com.topjohnwu.magisk.data.network.JSDelivrServices
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
class NetworkService( class NetworkService(
private val pages: GithubPageServices, private val pages: GithubPageServices,
private val raw: GithubRawServices, private val raw: RawServices,
private val jsd: JSDelivrServices, private val jsd: JSDelivrServices,
private val api: GithubApiServices private val api: GithubApiServices
) { ) {
suspend fun fetchUpdate() = try { suspend fun fetchUpdate() = safe {
var info = when (Config.updateChannel) { var info = when (Config.updateChannel) {
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate() DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
BETA_CHANNEL -> fetchBetaUpdate() BETA_CHANNEL -> fetchBetaUpdate()
@@ -38,31 +36,62 @@ class NetworkService(
} }
Info.remote = info Info.remote = info
info info
} catch (e: IOException) {
Timber.e(e)
null
} catch (e: HttpException) {
Timber.e(e)
null
} }
// UpdateInfo // UpdateInfo
suspend fun fetchStableUpdate() = pages.fetchStableUpdate() private suspend fun fetchStableUpdate() = pages.fetchStableUpdate()
suspend fun fetchBetaUpdate() = pages.fetchBetaUpdate() private suspend fun fetchBetaUpdate() = pages.fetchBetaUpdate()
suspend fun fetchCanaryUpdate() = raw.fetchCanaryUpdate() private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url) private suspend fun fetchCanaryUpdate(): UpdateInfo {
val sha = fetchCanaryVersion()
val info = jsd.fetchCanaryUpdate(sha)
// Byte streams fun genCDNUrl(name: String) = "${Const.Url.JS_DELIVR_URL}${MAGISK_FILES}@${sha}/${name}"
suspend fun fetchSafetynet() = jsd.fetchSafetynet() fun ManagerJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
suspend fun fetchBootctl() = jsd.fetchBootctl() fun MagiskJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
suspend fun fetchInstaller() = raw.fetchInstaller() fun StubJson.updateCopy() = copy(link = genCDNUrl(link))
suspend fun fetchFile(url: String) = raw.fetchFile(url) fun UninstallerJson.updateCopy() = copy(link = genCDNUrl(link))
// Strings return info.copy(
suspend fun fetchMetadata(repo: Repo) = raw.fetchModuleFile(repo.id, "module.prop") app = info.app.updateCopy(),
suspend fun fetchReadme(repo: Repo) = raw.fetchModuleFile(repo.id, "README.md") magisk = info.magisk.updateCopy(),
suspend fun fetchString(url: String) = raw.fetchString(url) stub = info.stub.updateCopy(),
uninstaller = info.uninstaller.updateCopy()
)
}
// API calls private inline fun <T> safe(factory: () -> T): T? {
suspend fun fetchRepos(page: Int, etag: String) = api.fetchRepos(page, etag) return try {
factory()
} catch (e: Exception) {
Timber.e(e)
null
}
}
private inline fun <T> wrap(factory: () -> T): T {
return try {
factory()
} catch (e: HttpException) {
throw IOException(e)
}
}
// Modules related
suspend fun fetchRepoInfo(url: String = Const.Url.OFFICIAL_REPO) = safe {
raw.fetchRepoInfo(url)
}
// Fetch files
suspend fun fetchSafetynet() = wrap { jsd.fetchSafetynet() }
suspend fun fetchBootctl() = wrap { jsd.fetchBootctl() }
suspend fun fetchInstaller() = wrap {
val sha = fetchMainVersion()
jsd.fetchInstaller(sha)
}
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
private suspend fun fetchCanaryVersion() = api.fetchBranch(MAGISK_FILES, "canary").commit.sha
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
} }

View File

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

View File

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

View File

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

View File

@@ -20,7 +20,7 @@ val viewModelModules = module {
viewModel { HideViewModel() } viewModel { HideViewModel() }
viewModel { HomeViewModel(get()) } viewModel { HomeViewModel(get()) }
viewModel { LogViewModel(get()) } viewModel { LogViewModel(get()) }
viewModel { ModuleViewModel(get(), get(), get()) } viewModel { ModuleViewModel(get(), get()) }
viewModel { SafetynetViewModel() } viewModel { SafetynetViewModel() }
viewModel { SettingsViewModel(get()) } viewModel { SettingsViewModel(get()) }
viewModel { SuperuserViewModel(get(), get()) } viewModel { SuperuserViewModel(get(), get()) }

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import android.app.Activity
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.topjohnwu.magisk.MainDirections import com.topjohnwu.magisk.MainDirections
@@ -12,7 +13,7 @@ import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.base.ActivityResultCallback import com.topjohnwu.magisk.core.base.ActivityResultCallback
import com.topjohnwu.magisk.core.base.BaseActivity import com.topjohnwu.magisk.core.base.BaseActivity
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MarkDownWindow import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
@@ -22,10 +23,10 @@ class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), Activi
override fun invoke(activity: BaseUIActivity<*, *>) = action(activity) override fun invoke(activity: BaseUIActivity<*, *>) = action(activity)
} }
class OpenChangelogEvent(val item: Repo) : ViewEventWithScope(), ContextExecutor { class OpenReadmeEvent(val item: OnlineModule) : ViewEventWithScope(), ContextExecutor {
override fun invoke(context: Context) { override fun invoke(context: Context) {
scope.launch { scope.launch {
MarkDownWindow.show(context, null, item::readme) MarkDownWindow.show(context, null, item::notes)
} }
} }
} }
@@ -58,9 +59,11 @@ class DieEvent : ViewEvent(), ActivityExecutor {
} }
} }
class ShowUIEvent : ViewEvent(), ActivityExecutor { class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
: ViewEvent(), ActivityExecutor {
override fun invoke(activity: BaseUIActivity<*, *>) { override fun invoke(activity: BaseUIActivity<*, *>) {
activity.setContentView() activity.setContentView()
activity.setAccessibilityDelegate(delegate)
} }
} }

View File

@@ -2,7 +2,6 @@ package com.topjohnwu.magisk.events.dialog
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.ktx.res import com.topjohnwu.magisk.ktx.res
@@ -16,7 +15,7 @@ class ManagerInstallDialog : DialogEvent() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
with(dialog) { with(dialog) {
val subject = Subject.Manager(Action.APK.Upgrade) val subject = Subject.Manager()
applyTitle(R.string.repo_install_title.res(R.string.app_name.res())) applyTitle(R.string.repo_install_title.res(R.string.app_name.res()))
applyMessage(R.string.repo_install_msg.res(subject.title)) applyMessage(R.string.repo_install_msg.res(subject.title))

View File

@@ -5,10 +5,10 @@ import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
class ModuleInstallDialog(private val item: Repo) : DialogEvent() { class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
override fun build(dialog: MagiskDialog) { override fun build(dialog: MagiskDialog) {
with(dialog) { with(dialog) {

View File

@@ -1,5 +1,6 @@
package com.topjohnwu.magisk.events.dialog package com.topjohnwu.magisk.events.dialog
import android.app.ProgressDialog
import android.widget.Toast import android.widget.Toast
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
@@ -17,8 +18,7 @@ class UninstallDialog : DialogEvent() {
.applyMessage(R.string.uninstall_magisk_msg) .applyMessage(R.string.uninstall_magisk_msg)
.applyButton(MagiskDialog.ButtonType.POSITIVE) { .applyButton(MagiskDialog.ButtonType.POSITIVE) {
titleRes = R.string.restore_img titleRes = R.string.restore_img
preventDismiss = true onClick { restore() }
onClick { restore(dialog) }
} }
if (Info.remote.uninstaller.link.isNotEmpty()) { if (Info.remote.uninstaller.link.isNotEmpty()) {
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) { dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
@@ -28,13 +28,12 @@ class UninstallDialog : DialogEvent() {
} }
} }
private fun restore(dialog: MagiskDialog) { @Suppress("DEPRECATION")
dialog.applyTitle(R.string.restore_img) private fun restore() {
.applyMessage(R.string.restore_img_msg) val dialog = ProgressDialog(dialog.context).apply {
.applyButton(MagiskDialog.ButtonType.POSITIVE) { setMessage(dialog.context.getString(R.string.restore_img_msg))
title = "" show()
} }
.cancellable(false)
Shell.su("restore_imgs").submit { result -> Shell.su("restore_imgs").submit { result ->
dialog.dismiss() dialog.dismiss()

View File

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

View File

@@ -1,5 +1,6 @@
package com.topjohnwu.magisk.ktx package com.topjohnwu.magisk.ktx
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
@@ -7,7 +8,6 @@ import android.content.ContextWrapper
import android.content.Intent import android.content.Intent
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.ComponentInfo import android.content.pm.ComponentInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.pm.PackageManager.* import android.content.pm.PackageManager.*
import android.content.res.Configuration import android.content.res.Configuration
@@ -57,29 +57,31 @@ import java.lang.reflect.Array as JArray
val packageName: String get() = get<Context>().packageName val packageName: String get() = get<Context>().packageName
val PackageInfo.processes val ApplicationInfo.processes: List<String> @SuppressLint("InlinedApi") get() {
get() = activities?.processNames.orEmpty() +
services?.processNames.orEmpty() +
receivers?.processNames.orEmpty() +
providers?.processNames.orEmpty()
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
val ApplicationInfo.packageInfo: PackageInfo get() {
val pm = get<PackageManager>() val pm = get<PackageManager>()
val appProcessName = processName ?: packageName
return try { val baseFlag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES
val packageInfo = try {
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
pm.getPackageInfo(packageName, request) pm.getPackageInfo(packageName, baseFlag or request)
} catch (e: NameNotFoundException) { // EdXposed hooked, issue#3276
return listOf(appProcessName)
} catch (e: Exception) { } catch (e: Exception) {
// Exceed binder data transfer limit, fetch each component type separately // Exceed binder data transfer limit, fetch each component type separately
pm.getPackageInfo(packageName, 0).apply { pm.getPackageInfo(packageName, baseFlag).apply {
runCatching { activities = pm.getPackageInfo(packageName, GET_ACTIVITIES).activities } runCatching { activities = pm.getPackageInfo(packageName, GET_ACTIVITIES).activities }
runCatching { services = pm.getPackageInfo(packageName, GET_SERVICES).services } runCatching { services = pm.getPackageInfo(packageName, GET_SERVICES).services }
runCatching { receivers = pm.getPackageInfo(packageName, GET_RECEIVERS).receivers } runCatching { receivers = pm.getPackageInfo(packageName, GET_RECEIVERS).receivers }
runCatching { providers = pm.getPackageInfo(packageName, GET_PROVIDERS).providers } runCatching { providers = pm.getPackageInfo(packageName, GET_PROVIDERS).providers }
} }
} }
fun Array<out ComponentInfo>.processNames() = map { it.processName ?: appProcessName }
return with(packageInfo) {
activities?.processNames().orEmpty() +
services?.processNames().orEmpty() +
receivers?.processNames().orEmpty() +
providers?.processNames().orEmpty()
}
} }
fun Context.rawResource(id: Int) = resources.openRawResource(id) fun Context.rawResource(id: Int) = resources.openRawResource(id)

View File

@@ -10,7 +10,6 @@ import android.view.WindowManager
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.setPadding
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@@ -26,9 +25,9 @@ import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
import com.topjohnwu.magisk.utils.HideableBehavior import com.topjohnwu.magisk.utils.HideableBehavior
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.MagiskDialog import com.topjohnwu.magisk.view.MagiskDialog
import com.topjohnwu.magisk.view.Shortcuts import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class MainViewModel : BaseViewModel() class MainViewModel : BaseViewModel()
@@ -39,14 +38,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
override val viewModel by viewModel<MainViewModel>() override val viewModel by viewModel<MainViewModel>()
override val navHost: Int = R.id.main_nav_host override val navHost: Int = R.id.main_nav_host
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
//implementation doesn't favor us against the design library, so on re-create it's often given
//upper hand.
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
binding.mainNavigation.setPadding(0)
}
private var isRootFragment = true private var isRootFragment = true
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -100,8 +91,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
(currentFragment as? ReselectionTarget)?.onReselected() (currentFragment as? ReselectionTarget)?.onReselected()
} }
binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
val section = if (intent.action == ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS val section = if (intent.action == ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
else intent.getStringExtra(Const.Key.OPEN_SECTION) else intent.getStringExtra(Const.Key.OPEN_SECTION)
getScreen(section)?.navigate() getScreen(section)?.navigate()
@@ -116,16 +105,11 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
binding.mainNavigation.menu.apply { binding.mainNavigation.menu.apply {
findItem(R.id.superuserFragment)?.isEnabled = Info.env.isActive findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
findItem(R.id.logFragment)?.isEnabled = Info.env.isActive findItem(R.id.logFragment)?.isEnabled = Info.env.isActive
} }
} }
override fun onDestroy() {
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
super.onDestroy()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
android.R.id.home -> onBackPressed() android.R.id.home -> onBackPressed()

View File

@@ -13,18 +13,17 @@ import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.tasks.FlashZip import com.topjohnwu.magisk.core.tasks.FlashZip
import com.topjohnwu.magisk.core.tasks.MagiskInstaller import com.topjohnwu.magisk.core.tasks.MagiskInstaller
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.databinding.RvBindingAdapter import com.topjohnwu.magisk.databinding.RvBindingAdapter
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.* import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.Notifications import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.CallbackList
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class FlashViewModel( class FlashViewModel(
args: FlashFragmentArgs, args: FlashFragmentArgs,
@@ -106,18 +105,16 @@ class FlashViewModel(
} }
private fun savePressed() = withExternalRW { private fun savePressed() = withExternalRW {
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) { val name = "magisk_install_log_%s.log".format(now.toTime(timeFormatStandard))
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard)) val file = MediaStoreUtils.getFile(name, true)
val file = MediaStoreUtils.getFile(name) file.uri.outputStream().bufferedWriter().use { writer ->
file.uri.outputStream().bufferedWriter().use { writer -> logItems.forEach {
logItems.forEach { writer.write(it)
writer.write(it) writer.newLine()
writer.newLine()
}
} }
SnackbarEvent(file.toString()).publish()
} }
SnackbarEvent(file.toString()).publish()
} }
} }

View File

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

View File

@@ -39,14 +39,19 @@ class HideItem(
get() = state get() = state
set(value) = set(value, state, { state = it }, BR.hiddenState) { set(value) = set(value, state, { state = it }, BR.hiddenState) {
if (value == true) { if (value == true) {
processes.filterNot { it.isHidden } processes
.filterNot { it.isHidden }
.filter { isExpanded || it.process.name == it.process.packageName }
} else { } else {
processes processes
.filter { it.isHidden }
.filter { isExpanded || it.process.name == it.process.packageName }
}.forEach { it.toggle() } }.forEach { it.toggle() }
} }
init { init {
processes.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } } processes.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } }
addOnPropertyChangedCallback(BR.expanded) { recalculateChecked() }
recalculateChecked() recalculateChecked()
} }
@@ -57,10 +62,14 @@ class HideItem(
private fun recalculateChecked() { private fun recalculateChecked() {
itemsChecked = processes.count { it.isHidden } itemsChecked = processes.count { it.isHidden }
state = when (itemsChecked) { state = if (isExpanded) {
0 -> false when (itemsChecked) {
processes.size -> true 0 -> false
else -> null processes.size -> true
else -> null
}
} else {
processes.find { it.isHidden && it.process.name == it.process.packageName } != null
} }
} }

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
package com.topjohnwu.magisk.ui.home package com.topjohnwu.magisk.ui.home
import android.os.Build
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BuildConfig import com.topjohnwu.magisk.BuildConfig
@@ -68,8 +67,7 @@ class HomeViewModel(
set(value) = set(value, field, { field = it }, BR.stateManagerProgress) set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
@get:Bindable @get:Bindable
val showUninstall get() = val showUninstall get() = Info.env.isActive && state != State.LOADING
Info.env.magiskVersionCode > 0 && stateMagisk != MagiskState.LOADING && isConnected.get()
@get:Bindable @get:Bindable
val showSafetyNet get() = Info.hasGMS && isConnected.get() val showSafetyNet get() = Info.hasGMS && isConnected.get()
@@ -82,8 +80,6 @@ class HomeViewModel(
override fun refresh() = viewModelScope.launch { override fun refresh() = viewModelScope.launch {
state = State.LOADING state = State.LOADING
notifyPropertyChanged(BR.showUninstall)
notifyPropertyChanged(BR.showSafetyNet)
svc.fetchUpdate()?.apply { svc.fetchUpdate()?.apply {
state = State.LOADED state = State.LOADED
stateMagisk = when { stateMagisk = when {
@@ -93,7 +89,6 @@ class HomeViewModel(
} }
stateManager = when { stateManager = when {
!app.isUpdateChannelCorrect && isConnected.get() -> MagiskState.NOT_INSTALLED
app.isObsolete -> MagiskState.OBSOLETE app.isObsolete -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE else -> MagiskState.UP_TO_DATE
} }
@@ -107,6 +102,8 @@ class HomeViewModel(
ensureEnv() ensureEnv()
} }
} ?: apply { state = State.LOADING_FAILED } } ?: apply { state = State.LOADING_FAILED }
notifyPropertyChanged(BR.showUninstall)
notifyPropertyChanged(BR.showSafetyNet)
} }
val showTest = false val showTest = false
@@ -127,14 +124,18 @@ class HomeViewModel(
fun onDeletePressed() = UninstallDialog().publish() fun onDeletePressed() = UninstallDialog().publish()
fun onManagerPressed() = fun onManagerPressed() = when (state) {
if (isConnected.get()) ManagerInstallDialog().publish() State.LOADED -> ManagerInstallDialog().publish()
else SnackbarEvent(R.string.no_connection).publish() State.LOADING -> SnackbarEvent(R.string.loading).publish()
else -> SnackbarEvent(R.string.no_connection).publish()
}
fun onMagiskPressed() = if (isConnected.get()) withExternalRW { fun onMagiskPressed() = when (state) {
HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish() State.LOADED -> withExternalRW {
} else { HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish()
SnackbarEvent(R.string.no_connection).publish() }
State.LOADING -> SnackbarEvent(R.string.loading).publish()
else -> SnackbarEvent(R.string.no_connection).publish()
} }
fun onSafetyNetPressed() = fun onSafetyNetPressed() =
@@ -150,17 +151,7 @@ class HomeViewModel(
MagiskState.NOT_INSTALLED, MagiskState.NOT_INSTALLED,
MagiskState.LOADING MagiskState.LOADING
) )
if (invalidStates.any { it == stateMagisk } || shownDialog) return
// Don't bother checking env when magisk is not installed, loading or already has been shown
if (
invalidStates.any { it == stateMagisk } ||
shownDialog ||
// don't care for emulators either
Build.DEVICE.orEmpty().contains("generic") ||
Build.PRODUCT.orEmpty().contains("generic")
) {
return
}
val result = Shell.su("env_check").await() val result = Shell.su("env_check").await()
if (!result.isSuccess) { if (!result.isSuccess) {
@@ -171,8 +162,6 @@ class HomeViewModel(
private val MagiskJson.isObsolete private val MagiskJson.isObsolete
get() = Info.env.isActive && Info.env.magiskVersionCode < versionCode get() = Info.env.isActive && Info.env.magiskVersionCode < versionCode
private val ManagerJson.isUpdateChannelCorrect
get() = versionCode > 0
private val ManagerJson.isObsolete private val ManagerJson.isObsolete
get() = BuildConfig.VERSION_CODE < versionCode get() = BuildConfig.VERSION_CODE < versionCode

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ import com.topjohnwu.magisk.utils.set
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.get import org.koin.core.get
import timber.log.Timber
import java.io.IOException
import kotlin.math.roundToInt import kotlin.math.roundToInt
class InstallViewModel( class InstallViewModel(
@@ -64,7 +66,11 @@ class InstallViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
notes = svc.fetchString(Info.remote.magisk.note) try {
notes = svc.fetchString(Info.remote.magisk.note)
} catch (e: IOException) {
Timber.e(e)
}
} }
} }
@@ -75,6 +81,8 @@ class InstallViewModel(
this.progress = progress.times(100).roundToInt() this.progress = progress.times(100).roundToInt()
if (this.progress >= 100) { if (this.progress >= 100) {
state = State.LOADED state = State.LOADED
} else if (this.progress < -150) {
state = State.LOADING_FAILED
} }
} }

View File

@@ -9,6 +9,9 @@ import androidx.core.view.isVisible
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.MotionRevealHelper import com.topjohnwu.magisk.utils.MotionRevealHelper
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -42,6 +45,21 @@ class LogFragment : BaseUIFragment<LogViewModel, FragmentLogMd2Binding>() {
binding.logFilterToggle.setOnClickListener { binding.logFilterToggle.setOnClickListener {
isMagiskLogVisible = true isMagiskLogVisible = true
} }
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.logFilterSuperuser.logSuperuser.addVerticalPadding(
0,
l1
)
binding.logFilterSuperuser.logSuperuser.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.logFilterSuperuser.logSuperuser.fixEdgeEffect()
} }

View File

@@ -3,20 +3,24 @@ package com.topjohnwu.magisk.ui.log
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseViewModel import com.topjohnwu.magisk.arch.BaseViewModel
import com.topjohnwu.magisk.arch.diffListOf import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.data.repository.LogRepository import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.ktx.now
import com.topjohnwu.magisk.ktx.timeFormatStandard
import com.topjohnwu.magisk.ktx.toTime
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
import com.topjohnwu.magisk.view.TextItem import com.topjohnwu.magisk.view.TextItem
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.*
class LogViewModel( class LogViewModel(
private val repo: LogRepository private val repo: LogRepository
@@ -54,14 +58,23 @@ class LogViewModel(
fun saveMagiskLog() = withExternalRW { fun saveMagiskLog() = withExternalRW {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val now = Calendar.getInstance() val filename = "magisk_log_%s.log".format(now.toTime(timeFormatStandard))
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format( val logFile = MediaStoreUtils.getFile(filename, true)
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1, logFile.uri.outputStream().bufferedWriter().use { file ->
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY), file.write("---System Properties---\n\n")
now.get(Calendar.MINUTE), now.get(Calendar.SECOND)
) ProcessBuilder("getprop").start()
val logFile = MediaStoreUtils.getFile(filename) .inputStream.reader().use { it.copyTo(file) }
logFile.uri.outputStream().writer().use { it.write(consoleText) }
file.write("\n---Magisk Logs---\n")
file.write("${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})\n\n")
file.write(consoleText)
file.write("\n---Manager Logs---\n")
file.write("${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n")
ProcessBuilder("logcat", "-d").start()
.inputStream.reader().use { it.copyTo(file) }
}
SnackbarEvent(logFile.toString()).publish() SnackbarEvent(logFile.toString()).publish()
} }
} }

View File

@@ -13,7 +13,7 @@ import com.topjohnwu.magisk.arch.ReselectionTarget
import com.topjohnwu.magisk.arch.ViewEvent import com.topjohnwu.magisk.arch.ViewEvent
import com.topjohnwu.magisk.core.download.BaseDownloader import com.topjohnwu.magisk.core.download.BaseDownloader
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
import com.topjohnwu.magisk.ktx.hideKeyboard import com.topjohnwu.magisk.ktx.*
import com.topjohnwu.magisk.ui.MainActivity import com.topjohnwu.magisk.ui.MainActivity
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
import com.topjohnwu.magisk.utils.MotionRevealHelper import com.topjohnwu.magisk.utils.MotionRevealHelper
@@ -62,6 +62,40 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard() if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
} }
}) })
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.moduleList.apply {
addVerticalPadding(
l_50,
l1 + l_50 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
)
addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
fixEdgeEffect()
post {
addInvalidateItemDecorationsObserver()
}
}
binding.moduleFilterInclude.moduleFilterList.apply {
addVerticalPadding(
l_50,
l_50
)
addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
fixEdgeEffect()
}
} }
override fun onDestroyView() { override fun onDestroyView() {

View File

@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.ui.module
import androidx.databinding.Bindable import androidx.databinding.Bindable
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.core.model.module.Module import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.model.module.Repo import com.topjohnwu.magisk.core.model.module.OnlineModule
import com.topjohnwu.magisk.databinding.ComparableRvItem import com.topjohnwu.magisk.databinding.ComparableRvItem
import com.topjohnwu.magisk.databinding.ObservableItem import com.topjohnwu.magisk.databinding.ObservableItem
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
@@ -39,7 +39,7 @@ class SectionTitle(
override fun contentSameAs(other: SectionTitle): Boolean = this === other override fun contentSameAs(other: SectionTitle): Boolean = this === other
} }
sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() { sealed class RepoItem(val item: OnlineModule) : ObservableItem<RepoItem>() {
override val layoutRes: Int = R.layout.item_repo_md2 override val layoutRes: Int = R.layout.item_repo_md2
@get:Bindable @get:Bindable
@@ -51,21 +51,21 @@ sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
override fun contentSameAs(other: RepoItem): Boolean = item == other.item override fun contentSameAs(other: RepoItem): Boolean = item == other.item
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id
class Update(item: Repo) : RepoItem(item) { class Update(item: OnlineModule) : RepoItem(item) {
override val isUpdate get() = true override val isUpdate get() = true
} }
class Remote(item: Repo) : RepoItem(item) { class Remote(item: OnlineModule) : RepoItem(item) {
override val isUpdate get() = false override val isUpdate get() = false
} }
} }
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>() { class ModuleItem(val item: LocalModule) : ObservableItem<ModuleItem>() {
override val layoutRes = R.layout.item_module_md2 override val layoutRes = R.layout.item_module_md2
@get:Bindable @get:Bindable
var repo: Repo? = null var repo: OnlineModule? = null
set(value) = set(value, field, { field = it }, BR.repo) set(value) = set(value, field, { field = it }, BR.repo)
@get:Bindable @get:Bindable

View File

@@ -9,13 +9,12 @@ import com.topjohnwu.magisk.arch.*
import com.topjohnwu.magisk.core.Config import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.model.module.Module import com.topjohnwu.magisk.core.model.module.LocalModule
import com.topjohnwu.magisk.core.tasks.RepoUpdater import com.topjohnwu.magisk.core.tasks.RepoUpdater
import com.topjohnwu.magisk.data.database.RepoByNameDao import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.data.database.RepoByUpdatedDao
import com.topjohnwu.magisk.databinding.RvItem import com.topjohnwu.magisk.databinding.RvItem
import com.topjohnwu.magisk.events.OpenReadmeEvent
import com.topjohnwu.magisk.events.SelectModuleEvent import com.topjohnwu.magisk.events.SelectModuleEvent
import com.topjohnwu.magisk.events.OpenChangelogEvent
import com.topjohnwu.magisk.events.SnackbarEvent import com.topjohnwu.magisk.events.SnackbarEvent
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
import com.topjohnwu.magisk.ktx.addOnListChangedCallback import com.topjohnwu.magisk.ktx.addOnListChangedCallback
@@ -44,8 +43,7 @@ import kotlin.math.roundToInt
* */ * */
class ModuleViewModel( class ModuleViewModel(
private val repoName: RepoByNameDao, private val repoDB: RepoDao,
private val repoUpdated: RepoByUpdatedDao,
private val repoUpdater: RepoUpdater private val repoUpdater: RepoUpdater
) : BaseViewModel(), Queryable { ) : BaseViewModel(), Queryable {
@@ -117,12 +115,6 @@ class ModuleViewModel(
// --- // ---
private var refetch = false private var refetch = false
private val dao
get() = when (Config.repoOrder) {
Config.Value.ORDER_DATE -> repoUpdated
Config.Value.ORDER_NAME -> repoName
else -> throw IllegalArgumentException()
}
// --- // ---
@@ -186,7 +178,7 @@ class ModuleViewModel(
} }
private suspend fun loadInstalled() { private suspend fun loadInstalled() {
val installed = Module.installed().map { ModuleItem(it) } val installed = LocalModule.installed().map { ModuleItem(it) }
val diff = withContext(Dispatchers.Default) { val diff = withContext(Dispatchers.Default) {
itemsInstalled.calculateDiff(installed) itemsInstalled.calculateDiff(installed)
} }
@@ -197,11 +189,11 @@ class ModuleViewModel(
val (updates, diff) = withContext(Dispatchers.IO) { val (updates, diff) = withContext(Dispatchers.IO) {
itemsInstalled.forEach { itemsInstalled.forEach {
launch { launch {
it.repo = dao.getRepoById(it.item.id) it.repo = repoDB.getModule(it.item.id)
} }
} }
val updates = itemsInstalled val updates = itemsInstalled
.mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) } .mapNotNull { repoDB.getUpdatableModule(it.item.id, it.item.versionCode) }
.map { RepoItem.Update(it) } .map { RepoItem.Update(it) }
val diff = itemsUpdatable.calculateDiff(updates) val diff = itemsUpdatable.calculateDiff(updates)
return@withContext updates to diff return@withContext updates to diff
@@ -219,7 +211,7 @@ class ModuleViewModel(
remoteJob = viewModelScope.launch { remoteJob = viewModelScope.launch {
suspend fun loadRemoteDB(offset: Int) = withContext(Dispatchers.IO) { suspend fun loadRemoteDB(offset: Int) = withContext(Dispatchers.IO) {
dao.getRepos(offset).map { RepoItem.Remote(it) } repoDB.getModules(offset).map { RepoItem.Remote(it) }
} }
isRemoteLoading = true isRemoteLoading = true
@@ -253,7 +245,7 @@ class ModuleViewModel(
listOf() listOf()
} else { } else {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
dao.searchRepos(query, offset).map { RepoItem.Remote(it) } repoDB.searchModules(query, offset).map { RepoItem.Remote(it) }
} }
} }
} }
@@ -315,14 +307,14 @@ class ModuleViewModel(
} }
fun infoPressed(item: RepoItem) = fun infoPressed(item: RepoItem) =
if (isConnected.get()) OpenChangelogEvent(item.item).publish() if (isConnected.get()) OpenReadmeEvent(item.item).publish()
else SnackbarEvent(R.string.no_connection).publish() else SnackbarEvent(R.string.no_connection).publish()
fun infoPressed(item: ModuleItem) { fun infoPressed(item: ModuleItem) {
item.repo?.also { item.repo?.also {
if (isConnected.get()) if (isConnected.get())
OpenChangelogEvent(it).publish() OpenReadmeEvent(it).publish()
else else
SnackbarEvent(R.string.no_connection).publish() SnackbarEvent(R.string.no_connection).publish()
} ?: return } ?: return

View File

@@ -158,7 +158,7 @@ sealed class BaseSettingsItem : ObservableItem<BaseSettingsItem>() {
runCatching { getStringArray(id) }.getOrDefault(emptyArray()) runCatching { getStringArray(id) }.getOrDefault(emptyArray())
override fun onPressed(view: View, callback: Callback) { override fun onPressed(view: View, callback: Callback) {
if (entries.isEmpty() || entryValues.isEmpty()) return if (entries.isEmpty()) return
super.onPressed(view, callback) super.onPressed(view, callback)
} }

View File

@@ -5,6 +5,9 @@ import android.view.View
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import com.topjohnwu.magisk.ktx.setOnViewReadyListener import com.topjohnwu.magisk.ktx.setOnViewReadyListener
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -24,6 +27,21 @@ class SettingsFragment : BaseUIFragment<SettingsViewModel, FragmentSettingsMd2Bi
binding.settingsList.setOnViewReadyListener { binding.settingsList.setOnViewReadyListener {
binding.settingsList.scrollToPosition(0) binding.settingsList.scrollToPosition(0)
} }
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.settingsList.addVerticalPadding(
0,
l1
)
binding.settingsList.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.settingsList.fixEdgeEffect()
} }
override fun onResume() { override fun onResume() {

View File

@@ -11,7 +11,7 @@ import com.topjohnwu.magisk.core.Config
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.UpdateCheckService import com.topjohnwu.magisk.core.UpdateCheckService
import com.topjohnwu.magisk.core.tasks.PatchAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.core.utils.BiometricHelper import com.topjohnwu.magisk.core.utils.BiometricHelper
import com.topjohnwu.magisk.core.utils.MediaStoreUtils import com.topjohnwu.magisk.core.utils.MediaStoreUtils
import com.topjohnwu.magisk.core.utils.availableLocales import com.topjohnwu.magisk.core.utils.availableLocales
@@ -20,6 +20,7 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
import com.topjohnwu.magisk.ktx.get import com.topjohnwu.magisk.ktx.get
import com.topjohnwu.magisk.utils.TransitiveText
import com.topjohnwu.magisk.utils.Utils import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.asTransitive import com.topjohnwu.magisk.utils.asTransitive
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
@@ -90,7 +91,7 @@ object Hide : BaseSettingsItem.Input() {
set(value) = set(value, field, { field = it }, BR.result, BR.error) set(value) = set(value, field, { field = it }, BR.result, BR.error)
val maxLength val maxLength
get() = PatchAPK.MAX_LABEL_LENGTH get() = HideAPK.MAX_LABEL_LENGTH
@get:Bindable @get:Bindable
val isError val isError
@@ -99,11 +100,17 @@ object Hide : BaseSettingsItem.Input() {
override fun getView(context: Context) = DialogSettingsAppNameBinding override fun getView(context: Context) = DialogSettingsAppNameBinding
.inflate(LayoutInflater.from(context)).also { it.data = this }.root .inflate(LayoutInflater.from(context)).also { it.data = this }.root
override fun refresh() {
isEnabled = Info.remote.stub.versionCode > 0
}
} }
object Restore : BaseSettingsItem.Blank() { object Restore : BaseSettingsItem.Blank() {
override val title = R.string.settings_restore_manager_title.asTransitive() override val title = R.string.settings_restore_manager_title.asTransitive()
override val description = R.string.settings_restore_manager_summary.asTransitive() override val description = R.string.settings_restore_manager_summary.asTransitive()
override fun refresh() {
isEnabled = Info.remote.app.versionCode > 0
}
} }
object AddShortcut : BaseSettingsItem.Blank() { object AddShortcut : BaseSettingsItem.Blank() {
@@ -137,11 +144,12 @@ object UpdateChannel : BaseSettingsItem.Selector() {
set(value) = setV(value, field, { field = it }) { Config.updateChannel = it } set(value) = setV(value, field, { field = it }) { Config.updateChannel = it }
override val title = R.string.settings_update_channel_title.asTransitive() override val title = R.string.settings_update_channel_title.asTransitive()
override val entries override val entries: Array<String> = resources.getStringArray(R.array.update_channel).let {
get() = resources.getStringArray(R.array.update_channel).let { if (BuildConfig.DEBUG) it.toMutableList().apply { add("Canary") }.toTypedArray() else it
if (BuildConfig.DEBUG) it.toMutableList().apply { add("Canary") }.toTypedArray() else it }
} override val description
override val entryValRes = R.array.value_array get() = entries.getOrNull(value)?.asTransitive()
?: TransitiveText.String(if (value == -1) entries[0] else "Canary")
} }
object UpdateChannelUrl : BaseSettingsItem.Input() { object UpdateChannelUrl : BaseSettingsItem.Input() {
@@ -189,6 +197,13 @@ object SystemlessHosts : BaseSettingsItem.Blank() {
override val description = R.string.settings_hosts_summary.asTransitive() override val description = R.string.settings_hosts_summary.asTransitive()
} }
object Tapjack : BaseSettingsItem.Toggle() {
override val title = R.string.settings_su_tapjack_title.asTransitive()
override var description = R.string.settings_su_tapjack_summary.asTransitive()
override var value = Config.suTapjack
set(value) = setV(value, field, { field = it }) { Config.suTapjack = it }
}
object Biometrics : BaseSettingsItem.Toggle() { object Biometrics : BaseSettingsItem.Toggle() {
override val title = R.string.settings_su_biometric_title.asTransitive() override val title = R.string.settings_su_biometric_title.asTransitive()
override var value = Config.suBiometric override var value = Config.suBiometric
@@ -243,22 +258,20 @@ object Superuser : BaseSettingsItem.Section() {
object AccessMode : BaseSettingsItem.Selector() { object AccessMode : BaseSettingsItem.Selector() {
override val title = R.string.superuser_access.asTransitive() override val title = R.string.superuser_access.asTransitive()
override val entryRes = R.array.su_access override val entryRes = R.array.su_access
override val entryValRes = R.array.value_array
override var value = Config.rootMode override var value = Config.rootMode
set(value) = setV(value, field, { field = it }) { set(value) = setV(value, field, { field = it }) {
Config.rootMode = entryValues[it].toInt() Config.rootMode = it
} }
} }
object MultiuserMode : BaseSettingsItem.Selector() { object MultiuserMode : BaseSettingsItem.Selector() {
override val title = R.string.multiuser_mode.asTransitive() override val title = R.string.multiuser_mode.asTransitive()
override val entryRes = R.array.multiuser_mode override val entryRes = R.array.multiuser_mode
override val entryValRes = R.array.value_array
override var value = Config.suMultiuserMode override var value = Config.suMultiuserMode
set(value) = setV(value, field, { field = it }) { set(value) = setV(value, field, { field = it }) {
Config.suMultiuserMode = entryValues[it].toInt() Config.suMultiuserMode = it
} }
override val description override val description
@@ -272,11 +285,10 @@ object MultiuserMode : BaseSettingsItem.Selector() {
object MountNamespaceMode : BaseSettingsItem.Selector() { object MountNamespaceMode : BaseSettingsItem.Selector() {
override val title = R.string.mount_namespace_mode.asTransitive() override val title = R.string.mount_namespace_mode.asTransitive()
override val entryRes = R.array.namespace override val entryRes = R.array.namespace
override val entryValRes = R.array.value_array
override var value = Config.suMntNamespaceMode override var value = Config.suMntNamespaceMode
set(value) = setV(value, field, { field = it }) { set(value) = setV(value, field, { field = it }) {
Config.suMntNamespaceMode = entryValues[it].toInt() Config.suMntNamespaceMode = it
} }
override val description override val description
@@ -286,11 +298,10 @@ object MountNamespaceMode : BaseSettingsItem.Selector() {
object AutomaticResponse : BaseSettingsItem.Selector() { object AutomaticResponse : BaseSettingsItem.Selector() {
override val title = R.string.auto_response.asTransitive() override val title = R.string.auto_response.asTransitive()
override val entryRes = R.array.auto_response override val entryRes = R.array.auto_response
override val entryValRes = R.array.value_array
override var value = Config.suAutoReponse override var value = Config.suAutoResponse
set(value) = setV(value, field, { field = it }) { set(value) = setV(value, field, { field = it }) {
Config.suAutoReponse = entryValues[it].toInt() Config.suAutoResponse = it
} }
} }
@@ -301,7 +312,7 @@ object RequestTimeout : BaseSettingsItem.Selector() {
override var value = selected override var value = selected
set(value) = setV(value, field, { field = it }) { set(value) = setV(value, field, { field = it }) {
Config.suDefaultTimeout = entryValues[it].toInt() Config.suDefaultTimeout = it
} }
private val selected: Int private val selected: Int
@@ -311,10 +322,9 @@ object RequestTimeout : BaseSettingsItem.Selector() {
object SUNotification : BaseSettingsItem.Selector() { object SUNotification : BaseSettingsItem.Selector() {
override val title = R.string.superuser_notification.asTransitive() override val title = R.string.superuser_notification.asTransitive()
override val entryRes = R.array.su_notification override val entryRes = R.array.su_notification
override val entryValRes = R.array.value_array
override var value = Config.suNotification override var value = Config.suNotification
set(value) = setV(value, field, { field = it }) { set(value) = setV(value, field, { field = it }) {
Config.suNotification = entryValues[it].toInt() Config.suNotification = it
} }
} }

View File

@@ -15,11 +15,8 @@ import com.topjohnwu.magisk.arch.diffListOf
import com.topjohnwu.magisk.arch.itemBindingOf import com.topjohnwu.magisk.arch.itemBindingOf
import com.topjohnwu.magisk.core.Const import com.topjohnwu.magisk.core.Const
import com.topjohnwu.magisk.core.Info import com.topjohnwu.magisk.core.Info
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.isRunningAsStub import com.topjohnwu.magisk.core.isRunningAsStub
import com.topjohnwu.magisk.core.tasks.PatchAPK import com.topjohnwu.magisk.core.tasks.HideAPK
import com.topjohnwu.magisk.data.database.RepoDao import com.topjohnwu.magisk.data.database.RepoDao
import com.topjohnwu.magisk.events.AddHomeIconEvent import com.topjohnwu.magisk.events.AddHomeIconEvent
import com.topjohnwu.magisk.events.RecreateEvent import com.topjohnwu.magisk.events.RecreateEvent
@@ -82,7 +79,7 @@ class SettingsViewModel(
if (Utils.showSuperUser()) { if (Utils.showSuperUser()) {
list.addAll(listOf( list.addAll(listOf(
Superuser, Superuser,
Biometrics, AccessMode, MultiuserMode, MountNamespaceMode, Tapjack, Biometrics, AccessMode, MultiuserMode, MountNamespaceMode,
AutomaticResponse, RequestTimeout, SUNotification AutomaticResponse, RequestTimeout, SUNotification
)) ))
if (Build.VERSION.SDK_INT < 23) { if (Build.VERSION.SDK_INT < 23) {
@@ -104,7 +101,7 @@ class SettingsViewModel(
is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish() is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish()
is ClearRepoCache -> clearRepoCache() is ClearRepoCache -> clearRepoCache()
is SystemlessHosts -> createHosts() is SystemlessHosts -> createHosts()
is Restore -> restoreManager() is Restore -> HideAPK.restore(view.context)
is AddShortcut -> AddHomeIconEvent().publish() is AddShortcut -> AddHomeIconEvent().publish()
else -> callback() else -> callback()
} }
@@ -112,7 +109,7 @@ class SettingsViewModel(
override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) { override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) {
is Language -> RecreateEvent().publish() is Language -> RecreateEvent().publish()
is UpdateChannel -> openUrlIfNecessary(view) is UpdateChannel -> openUrlIfNecessary(view)
is Hide -> PatchAPK.hideManager(view.context, item.value) is Hide -> HideAPK.hide(view.context, item.value)
else -> Unit else -> Unit
} }
@@ -142,9 +139,4 @@ class SettingsViewModel(
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT) Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
} }
} }
private fun restoreManager() {
DownloadService.start(get(), Subject.Manager(Action.APK.Restore))
}
} }

View File

@@ -1,8 +1,13 @@
package com.topjohnwu.magisk.ui.superuser package com.topjohnwu.magisk.ui.superuser
import android.os.Bundle
import android.view.View
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.BaseUIFragment import com.topjohnwu.magisk.arch.BaseUIFragment
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
import com.topjohnwu.magisk.ktx.addVerticalPadding
import com.topjohnwu.magisk.ktx.fixEdgeEffect
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() { class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
@@ -15,6 +20,25 @@ class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd
activity.title = resources.getString(R.string.superuser) activity.title = resources.getString(R.string.superuser)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val resource = requireContext().resources
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
binding.superuserList.addVerticalPadding(
l_50,
l1
)
binding.superuserList.addSimpleItemDecoration(
left = l1,
top = l_50,
right = l1,
bottom = l_50,
)
binding.superuserList.fixEdgeEffect()
}
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {} override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
} }

View File

@@ -28,8 +28,8 @@ open class SuRequestActivity : BaseUIActivity<SuRequestViewModel, ActivityReques
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
supportRequestWindowFeature(Window.FEATURE_NO_TITLE) supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
lockOrientation() lockOrientation()
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
fun showRequest() { fun showRequest() {

View File

@@ -1,11 +1,20 @@
package com.topjohnwu.magisk.ui.surequest package com.topjohnwu.magisk.ui.surequest
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Bundle
import android.os.CountDownTimer import android.os.CountDownTimer
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeProvider
import android.widget.Toast
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
@@ -22,6 +31,7 @@ import com.topjohnwu.magisk.events.DieEvent
import com.topjohnwu.magisk.events.ShowUIEvent import com.topjohnwu.magisk.events.ShowUIEvent
import com.topjohnwu.magisk.events.dialog.BiometricEvent import com.topjohnwu.magisk.events.dialog.BiometricEvent
import com.topjohnwu.magisk.ui.superuser.SpinnerRvItem import com.topjohnwu.magisk.ui.superuser.SpinnerRvItem
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.set import com.topjohnwu.magisk.utils.set
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
@@ -51,6 +61,19 @@ class SuRequestViewModel(
var grantEnabled = false var grantEnabled = false
set(value) = set(value, field, { field = it }, BR.grantEnabled) set(value) = set(value, field, { field = it }, BR.grantEnabled)
@SuppressLint("ClickableViewAccessibility")
val grantTouchListener = View.OnTouchListener { _: View, event: MotionEvent ->
// Filter obscured touches by consuming them.
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
|| event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0) {
if (event.action == MotionEvent.ACTION_UP) {
Utils.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
}
return@OnTouchListener Config.suTapjack
}
false
}
private val items = res.getStringArray(R.array.allow_timeout).map { SpinnerRvItem(it) } private val items = res.getStringArray(R.array.allow_timeout).map { SpinnerRvItem(it) }
val adapter = BindingListViewAdapter<SpinnerRvItem>(1).apply { val adapter = BindingListViewAdapter<SpinnerRvItem>(1).apply {
itemBinding = ItemBinding.of { binding, _, item -> itemBinding = ItemBinding.of { binding, _, item ->
@@ -104,7 +127,7 @@ class SuRequestViewModel(
timer = SuTimer(millis, 1000).apply { start() } timer = SuTimer(millis, 1000).apply { start() }
// Actually show the UI // Actually show the UI
ShowUIEvent().publish() ShowUIEvent(if (Config.suTapjack) EmptyAccessibilityDelegate else null).publish()
} }
private fun respond(action: Int) { private fun respond(action: Int) {
@@ -141,4 +164,18 @@ class SuRequestViewModel(
} }
} }
// Invisible for accessibility services
object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
override fun sendAccessibilityEvent(host: View?, eventType: Int) {}
override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?) = true
override fun sendAccessibilityEventUnchecked(host: View?, event: AccessibilityEvent?) {}
override fun dispatchPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) = true
override fun onPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) {}
override fun onInitializeAccessibilityEvent(host: View?, event: AccessibilityEvent?) {}
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
override fun onRequestSendAccessibilityEvent(host: ViewGroup?, child: View?, event: AccessibilityEvent?): Boolean = false
override fun getAccessibilityNodeProvider(host: View?): AccessibilityNodeProvider? = null
}
} }

View File

@@ -55,15 +55,15 @@ class EndlessRecyclerScrollListener(
val visibleItemPosition = if (direction == Direction.BOTTOM) { val visibleItemPosition = if (direction == Direction.BOTTOM) {
when (layoutManager) { when (layoutManager) {
is StaggeredGridLayoutManager -> is StaggeredGridLayoutManager ->
layoutManager.findLastVisibleItemPositions(null).max() ?: 0 layoutManager.findLastVisibleItemPositions(null).maxOrNull() ?: 0
is GridLayoutManager -> layoutManager.findLastVisibleItemPosition() is GridLayoutManager -> layoutManager.findLastVisibleItemPosition()
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition() is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported") else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
} }
} else { } else {
when (layoutManager) { when (layoutManager) {
is StaggeredGridLayoutManager -> layoutManager.findFirstVisibleItemPositions(null).min() is StaggeredGridLayoutManager ->
?: 0 layoutManager.findFirstVisibleItemPositions(null).minOrNull() ?: 0
is GridLayoutManager -> layoutManager.findFirstVisibleItemPosition() is GridLayoutManager -> layoutManager.findFirstVisibleItemPosition()
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition() is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported") else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")

View File

@@ -2,25 +2,24 @@ package com.topjohnwu.magisk.view
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Color import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.InsetDrawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatDialog import androidx.appcompat.app.AppCompatDialog
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding
import androidx.databinding.Bindable import androidx.databinding.Bindable
import androidx.databinding.PropertyChangeRegistry import androidx.databinding.PropertyChangeRegistry
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.google.android.material.shape.MaterialShapeDrawable
import com.topjohnwu.magisk.BR import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.arch.itemBindingOf import com.topjohnwu.magisk.arch.itemBindingOf
@@ -47,36 +46,22 @@ class MagiskDialog(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
super.setContentView(binding.root) super.setContentView(binding.root)
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, javaClass.canonicalName)
val materialShapeDrawable = MaterialShapeDrawable(context, null, R.attr.alertDialogStyle, R.style.MaterialAlertDialog_MaterialComponents)
materialShapeDrawable.initializeElevationOverlay(context)
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
val inset = context.resources.getDimensionPixelSize(R.dimen.appcompat_dialog_background_inset)
window?.apply { window?.apply {
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
setLayout( setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT
)
}
val paddingTop = binding.root.paddingTop
val paddingBottom = binding.root.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
view.updatePadding(
top = paddingTop + insets.systemWindowInsetTop,
bottom = paddingBottom + insets.systemWindowInsetBottom
)
insets
} }
} }
override fun setCancelable(flag: Boolean) { inner class Data : ObservableHost {
val listener = if (!flag) {
null
} else {
setCanceledOnTouchOutside(true)
View.OnClickListener { dismiss() }
}
binding.dialogBaseOutsideContainer.setOnClickListener(listener)
}
inner class Data: ObservableHost {
override var callbacks: PropertyChangeRegistry? = null override var callbacks: PropertyChangeRegistry? = null
@get:Bindable @get:Bindable
@@ -88,7 +73,7 @@ class MagiskDialog(
set(value) = set(value, field, { field = it }, BR.title) set(value) = set(value, field, { field = it }, BR.title)
@get:Bindable @get:Bindable
var message : CharSequence = "" var message: CharSequence = ""
set(value) = set(value, field, { field = it }, BR.message) set(value) = set(value, field, { field = it }, BR.message)
val buttonPositive = Button() val buttonPositive = Button()
@@ -101,7 +86,7 @@ class MagiskDialog(
POSITIVE, NEUTRAL, NEGATIVE, IDGAF POSITIVE, NEUTRAL, NEGATIVE, IDGAF
} }
inner class Button: ObservableHost { inner class Button : ObservableHost {
override var callbacks: PropertyChangeRegistry? = null override var callbacks: PropertyChangeRegistry? = null
@get:Bindable @get:Bindable
@@ -228,7 +213,7 @@ class MagiskDialog(
dismiss() dismiss()
} }
} }
val items = list.mapIndexed { i, it -> DialogItem(it, i) } val items = list.mapIndexed { i, cs -> DialogItem(cs, i) }
val binding = itemBindingOf<DialogItem> { it.bindExtra(BR.listener, actualListener) } val binding = itemBindingOf<DialogItem> { it.bindExtra(BR.listener, actualListener) }
.let { ItemBinding.of(it) } .let { ItemBinding.of(it) }

View File

@@ -15,7 +15,6 @@ import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
import com.topjohnwu.magisk.core.SplashActivity import com.topjohnwu.magisk.core.SplashActivity
import com.topjohnwu.magisk.core.cmp import com.topjohnwu.magisk.core.cmp
import com.topjohnwu.magisk.core.download.Action
import com.topjohnwu.magisk.core.download.DownloadService import com.topjohnwu.magisk.core.download.DownloadService
import com.topjohnwu.magisk.core.download.Subject import com.topjohnwu.magisk.core.download.Subject
import com.topjohnwu.magisk.core.intent import com.topjohnwu.magisk.core.intent
@@ -46,7 +45,6 @@ object Notifications {
setChannelId(UPDATE_NOTIFICATION_CHANNEL) setChannelId(UPDATE_NOTIFICATION_CHANNEL)
} else { } else {
setSmallIcon(R.drawable.ic_magisk_outline) setSmallIcon(R.drawable.ic_magisk_outline)
setVibrate(longArrayOf(0, 100, 100, 100))
} }
} }
} }
@@ -70,7 +68,7 @@ object Notifications {
} }
fun managerUpdate(context: Context) { fun managerUpdate(context: Context) {
val intent = DownloadService.pendingIntent(context, Subject.Manager(Action.APK.Upgrade)) val intent = DownloadService.pendingIntent(context, Subject.Manager())
val builder = updateBuilder(context) val builder = updateBuilder(context)
.setContentTitle(context.getString(R.string.manager_update_title)) .setContentTitle(context.getString(R.string.manager_update_title))

View File

@@ -1,5 +1,8 @@
package com.topjohnwu.signing; package com.topjohnwu.signing;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1InputStream;
@@ -14,6 +17,7 @@ import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FilterInputStream; import java.io.FilterInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -84,8 +88,10 @@ public class SignBoot {
} }
} }
public static boolean doSignature(String target, InputStream imgIn, OutputStream imgOut, public static boolean doSignature(
InputStream cert, InputStream key) { @Nullable X509Certificate cert, @Nullable PrivateKey key,
@NonNull InputStream imgIn, @NonNull OutputStream imgOut, @NonNull String target
) {
try { try {
PushBackRWStream in = new PushBackRWStream(imgIn, imgOut); PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM]; byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
@@ -96,16 +102,16 @@ public class SignBoot {
in.unread(hdr); in.unread(hdr);
BootSignature bootsig = new BootSignature(target, signableSize); BootSignature bootsig = new BootSignature(target, signableSize);
if (cert == null) { if (cert == null) {
cert = SignBoot.class.getResourceAsStream("/keys/verity.x509.pem"); cert = CryptoUtils.readCertificate(
new ByteArrayInputStream(KeyData.verityCert()));
} }
X509Certificate certificate = CryptoUtils.readCertificate(cert); bootsig.setCertificate(cert);
bootsig.setCertificate(certificate);
if (key == null) { if (key == null) {
key = SignBoot.class.getResourceAsStream("/keys/verity.pk8"); key = CryptoUtils.readPrivateKey(
new ByteArrayInputStream(KeyData.verityKey()));
} }
PrivateKey privateKey = CryptoUtils.readPrivateKey(key); byte[] sig = bootsig.sign(key, in, signableSize);
byte[] sig = bootsig.sign(privateKey, in, signableSize); bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(key));
bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(privateKey));
byte[] encoded_bootsig = bootsig.getEncoded(); byte[] encoded_bootsig = bootsig.getEncoded();
imgOut.write(encoded_bootsig); imgOut.write(encoded_bootsig);
imgOut.flush(); imgOut.flush();
@@ -116,7 +122,7 @@ public class SignBoot {
} }
} }
public static boolean verifySignature(InputStream imgIn, InputStream certIn) { public static boolean verifySignature(InputStream imgIn, X509Certificate cert) {
try { try {
// Read the header for size // Read the header for size
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM]; byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
@@ -142,8 +148,8 @@ public class SignBoot {
} }
BootSignature bootsig = new BootSignature(signature); BootSignature bootsig = new BootSignature(signature);
if (certIn != null) { if (cert != null) {
bootsig.setCertificate(CryptoUtils.readCertificate(certIn)); bootsig.setCertificate(cert);
} }
if (bootsig.verify(rawImg, signableSize)) { if (bootsig.verify(rawImg, signableSize)) {
System.err.println("Signature is VALID"); System.err.println("Signature is VALID");
@@ -314,4 +320,46 @@ public class SignBoot {
} }
} }
public static void main(String[] args) throws Exception {
if (args.length > 0 && "-verify".equals(args[0])) {
X509Certificate cert = null;
if (args.length >= 2) {
// args[1] is the path to a public key certificate
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
}
boolean signed = SignBoot.verifySignature(System.in, cert);
System.exit(signed ? 0 : 1);
} else if (args.length > 0 && "-sign".equals(args[0])) {
X509Certificate cert = null;
PrivateKey key = null;
String name = "/boot";
if (args.length >= 3) {
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
key = CryptoUtils.readPrivateKey(new FileInputStream(args[2]));
}
if (args.length == 2) {
name = args[1];
} else if (args.length >= 4) {
name = args[3];
}
boolean result = SignBoot.doSignature(cert, key, System.in, System.out, name);
System.exit(result ? 0 : 1);
} else {
System.err.println(
"BootSigner <actions> [args]\n" +
"Input from stdin, output to stdout\n" +
"\n" +
"Actions:\n" +
" -verify [x509.pem]\n" +
" verify image. cert is optional.\n" +
" -sign [x509.pem] [pk8] [name]\n" +
" sign image. name and the cert/key pair are optional.\n" +
" name should be either /boot (default) or /recovery.\n"
);
}
}
} }

View File

@@ -17,8 +17,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="@{viewModel.insets.left}" app:consumeSystemWindowsInsets="start|end"
android:paddingRight="@{viewModel.insets.right}" app:edgeToEdge="true"
app:fitsSystemWindowsInsets="start|end"
tools:ignore="RtlHardcoded"> tools:ignore="RtlHardcoded">
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
@@ -34,7 +35,7 @@
style="@style/WidgetFoundation.Appbar" style="@style/WidgetFoundation.Appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="@{viewModel.insets.top}"> app:fitsSystemWindowsInsets="top">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/main_toolbar" android:id="@+id/main_toolbar"
@@ -72,7 +73,9 @@
android:layout_gravity="bottom|center_horizontal" android:layout_gravity="bottom|center_horizontal"
android:layout_marginStart="@dimen/l1" android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1" android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{(int) @dimen/l1 + viewModel.insets.bottom}" android:layout_marginBottom="@dimen/l1"
android:fitsSystemWindows="false"
app:layout_fitsSystemWindowsInsets="bottom"
tools:layout_marginBottom="64dp"> tools:layout_marginBottom="64dp">
<com.google.android.material.bottomnavigation.BottomNavigationView <com.google.android.material.bottomnavigation.BottomNavigationView
@@ -81,6 +84,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent" android:background="@android:color/transparent"
android:textStyle="bold" android:textStyle="bold"
android:fitsSystemWindows="false"
android:paddingBottom="0dp"
app:fitsSystemWindowsInsets="start|end"
app:elevation="0dp" app:elevation="0dp"
app:itemHorizontalTranslationEnabled="false" app:itemHorizontalTranslationEnabled="false"
app:itemIconTint="@color/color_menu_tint" app:itemIconTint="@color/color_menu_tint"

View File

@@ -138,6 +138,7 @@
<Button <Button
android:id="@+id/grant_btn" android:id="@+id/grant_btn"
style="@style/WidgetFoundation.Button.Text" style="@style/WidgetFoundation.Button.Text"
onTouch="@{viewModel.grantTouchListener}"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"

View File

@@ -12,219 +12,185 @@
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/dialog_base_outside_container" android:layout_width="wrap_content"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="match_parent"> tools:layout_width="match_parent">
<FrameLayout <androidx.constraintlayout.widget.Guideline
android:layout_width="0dp" android:id="@+id/dialog_base_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constrainedHeight="true" android:orientation="vertical"
app:layout_constrainedWidth="true" app:layout_constraintGuide_begin="16dp" />
app:layout_constraintBottom_toBottomOf="parent"
<androidx.constraintlayout.widget.Guideline
android:id="@+id/dialog_base_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<ImageView
android:id="@+id/dialog_base_icon"
style="@style/WidgetFoundation.Image.Big"
gone="@{data.icon == null}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/l1"
android:src="@{data.icon}"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="400dp"> tools:src="@drawable/ic_delete_md2" />
<com.google.android.material.card.MaterialCardView <TextView
style="@style/WidgetFoundation.Card.Elevated" android:id="@+id/dialog_base_title"
gone="@{data.title.length == 0}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l1"
android:gravity="center"
android:text="@{data.title}"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_icon"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.core.widget.NestedScrollView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/l_50"
android:overScrollMode="ifContentScrolls"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_space"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_title">
<FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:focusable="false"
app:cardElevation="@dimen/margin_generic"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout <TextView
android:id="@+id/dialog_base_message"
gone="@{data.message.length == 0}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="match_parent"
android:text="@{data.message}"
android:textAppearance="@style/AppearanceFoundation.Body"
tools:lines="3"
tools:text="@tools:sample/lorem/random" />
<androidx.constraintlayout.widget.Guideline <FrameLayout
android:id="@+id/dialog_base_start" android:id="@+id/dialog_base_container"
android:layout_width="wrap_content" gone="@{data.message.length != 0}"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:orientation="vertical" android:layout_height="wrap_content" />
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline </FrameLayout>
android:id="@+id/dialog_base_end"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
<ImageView </androidx.core.widget.NestedScrollView>
android:id="@+id/dialog_base_icon"
style="@style/WidgetFoundation.Image.Big"
gone="@{data.icon == null}"
android:layout_gravity="center"
android:layout_marginTop="@dimen/l1"
android:src="@{data.icon}"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_title"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_delete_md2" />
<TextView <Space
android:id="@+id/dialog_base_title" android:id="@+id/dialog_base_space"
gone="@{data.title.length == 0}" android:layout_width="wrap_content"
android:layout_width="0dp" android:layout_height="@dimen/l_50"
android:layout_height="wrap_content" app:layout_constraintBottom_toTopOf="@+id/dialog_base_buttons"
android:layout_marginTop="@dimen/l1" app:layout_constraintEnd_toEndOf="parent"
android:gravity="center" app:layout_constraintStart_toStartOf="parent" />
android:text="@{data.title}"
android:textAppearance="@style/AppearanceFoundation.Title"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_icon"
tools:lines="1"
tools:text="@tools:sample/lorem/random" />
<androidx.core.widget.NestedScrollView <androidx.appcompat.widget.ButtonBarLayout
android:layout_width="0dp" android:id="@+id/dialog_base_buttons"
android:layout_height="wrap_content" android:layout_width="0dp"
android:layout_marginTop="@dimen/l_50" android:layout_height="wrap_content"
android:overScrollMode="ifContentScrolls" android:gravity="bottom|center_horizontal"
app:layout_constrainedHeight="true" android:layoutDirection="locale"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_space" android:orientation="horizontal"
app:layout_constraintEnd_toEndOf="@+id/dialog_base_end" android:paddingStart="12dp"
app:layout_constraintStart_toStartOf="@+id/dialog_base_start" android:paddingTop="4dp"
app:layout_constraintTop_toBottomOf="@+id/dialog_base_title"> android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<FrameLayout <Button
android:layout_width="match_parent" android:id="@+id/dialog_base_button_4"
android:layout_height="wrap_content"> style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonIDGAF.icon == 0 &amp;&amp; data.buttonIDGAF.title.length == 0}"
isEnabled="@{data.buttonIDGAF.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonIDGAF.isEnabled()}"
android:focusable="@{data.buttonIDGAF.isEnabled()}"
android:onClick="@{() -> data.buttonIDGAF.clicked()}"
android:text="@{data.buttonIDGAF.title}"
app:icon="@{data.buttonIDGAF.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<TextView <Button
android:id="@+id/dialog_base_message" android:id="@+id/dialog_base_button_2"
gone="@{data.message.length == 0}" style="@style/WidgetFoundation.Button.Text"
android:layout_width="match_parent" gone="@{data.buttonNeutral.icon == 0 &amp;&amp; data.buttonNeutral.title.length == 0}"
android:layout_height="match_parent" isEnabled="@{data.buttonNeutral.isEnabled()}"
android:text="@{data.message}" android:layout_width="wrap_content"
android:textAppearance="@style/AppearanceFoundation.Body" android:layout_height="wrap_content"
tools:lines="3" android:layout_gravity="center_horizontal"
tools:text="@tools:sample/lorem/random" /> android:clickable="@{data.buttonNeutral.isEnabled()}"
android:focusable="@{data.buttonNeutral.isEnabled()}"
android:onClick="@{() -> data.buttonNeutral.clicked()}"
android:text="@{data.buttonNeutral.title}"
app:icon="@{data.buttonNeutral.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<FrameLayout <Space
android:id="@+id/dialog_base_container" android:id="@+id/spacer"
gone="@{data.message.length != 0}" android:layout_width="0dp"
android:layout_width="match_parent" android:layout_height="0dp"
android:layout_height="wrap_content" /> android:layout_weight="1"
android:visibility="invisible" />
</FrameLayout> <Button
android:id="@+id/dialog_base_button_3"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNegative.icon == 0 &amp;&amp; data.buttonNegative.title.length == 0}"
isEnabled="@{data.buttonNegative.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonNegative.isEnabled()}"
android:focusable="@{data.buttonNegative.isEnabled()}"
android:onClick="@{() -> data.buttonNegative.clicked()}"
android:text="@{data.buttonNegative.title}"
app:icon="@{data.buttonNegative.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
</androidx.core.widget.NestedScrollView> <Button
android:id="@+id/dialog_base_button_1"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonPositive.icon == 0 &amp;&amp; data.buttonPositive.title.length == 0}"
isEnabled="@{data.buttonPositive.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonPositive.isEnabled()}"
android:focusable="@{data.buttonPositive.isEnabled()}"
android:onClick="@{() -> data.buttonPositive.clicked()}"
android:text="@{data.buttonPositive.title}"
app:icon="@{data.buttonPositive.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Space </androidx.appcompat.widget.ButtonBarLayout>
android:id="@+id/dialog_base_space"
android:layout_width="wrap_content"
android:layout_height="@dimen/l_50"
app:layout_constraintBottom_toTopOf="@+id/dialog_base_buttons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<androidx.appcompat.widget.ButtonBarLayout
android:id="@+id/dialog_base_buttons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="bottom|center_horizontal"
android:layoutDirection="locale"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/dialog_base_button_4"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonIDGAF.icon == 0 &amp;&amp; data.buttonIDGAF.title.length == 0}"
isEnabled="@{data.buttonIDGAF.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonIDGAF.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonIDGAF.isEnabled()}"
android:onClick="@{() -> data.buttonIDGAF.clicked()}"
android:text="@{data.buttonIDGAF.title}"
app:icon="@{data.buttonIDGAF.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Button
android:id="@+id/dialog_base_button_2"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNeutral.icon == 0 &amp;&amp; data.buttonNeutral.title.length == 0}"
isEnabled="@{data.buttonNeutral.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonNeutral.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonNeutral.isEnabled()}"
android:onClick="@{() -> data.buttonNeutral.clicked()}"
android:text="@{data.buttonNeutral.title}"
app:icon="@{data.buttonNeutral.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Space
android:id="@+id/spacer"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"
android:visibility="invisible" />
<Button
android:id="@+id/dialog_base_button_3"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonNegative.icon == 0 &amp;&amp; data.buttonNegative.title.length == 0}"
isEnabled="@{data.buttonNegative.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonNegative.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonNegative.isEnabled()}"
android:onClick="@{() -> data.buttonNegative.clicked()}"
android:text="@{data.buttonNegative.title}"
app:icon="@{data.buttonNegative.icon}"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
<Button
android:id="@+id/dialog_base_button_1"
style="@style/WidgetFoundation.Button.Text"
gone="@{data.buttonPositive.icon == 0 &amp;&amp; data.buttonPositive.title.length == 0}"
isEnabled="@{data.buttonPositive.isEnabled()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:clickable="@{data.buttonPositive.isEnabled()}"
android:filterTouchesWhenObscured="true"
android:focusable="@{data.buttonPositive.isEnabled()}"
android:onClick="@{() -> data.buttonPositive.clicked()}"
android:text="@{data.buttonPositive.title}"
app:icon="@{data.buttonPositive.icon}"
app:iconGravity="textStart"
tools:icon="@drawable/ic_bug_md2"
tools:text="Button 1" />
</androidx.appcompat.widget.ButtonBarLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</layout> </layout>

View File

@@ -18,7 +18,8 @@
<HorizontalScrollView <HorizontalScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}" android:layout_marginTop="@dimen/internal_action_bar_size"
app:layout_fitsSystemWindowsInsets="top"
tools:layout_marginTop="@dimen/internal_action_bar_size"> tools:layout_marginTop="@dimen/internal_action_bar_size">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
@@ -31,9 +32,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="@{viewModel.insets.left}" app:fitsSystemWindowsInsets="start|end|bottom"
android:paddingRight="@{viewModel.insets.right}"
android:paddingBottom="@{viewModel.insets.bottom}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_console_md2" /> tools:listitem="@layout/item_console_md2" />
@@ -46,12 +45,13 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_margin="@dimen/l1" android:layout_margin="@dimen/l1"
android:layout_marginBottom="@{(int) @dimen/l1 + viewModel.insets.bottom}" android:layout_marginBottom="@dimen/l1"
android:onClick="@{() -> viewModel.restartPressed()}" android:onClick="@{() -> viewModel.restartPressed()}"
android:text="@string/reboot" android:text="@string/reboot"
android:textAllCaps="false" android:textAllCaps="false"
android:textColor="?colorOnPrimary" android:textColor="?colorOnPrimary"
android:textStyle="bold" android:textStyle="bold"
app:layout_fitsSystemWindowsInsets="bottom"
app:backgroundTint="?colorPrimary" app:backgroundTint="?colorPrimary"
app:icon="@drawable/ic_restart" app:icon="@drawable/ic_restart"
app:iconTint="?colorOnPrimary" /> app:iconTint="?colorOnPrimary" />

View File

@@ -17,7 +17,6 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/hide_content" android:id="@+id/hide_content"
dividerVertical="@{@drawable/divider_l_50}"
invisibleUnless="@{viewModel.loaded || !viewModel.items.empty}" invisibleUnless="@{viewModel.loaded || !viewModel.items.empty}"
itemBinding="@{viewModel.itemBinding}" itemBinding="@{viewModel.itemBinding}"
items="@{viewModel.items}" items="@{viewModel.items}"
@@ -25,10 +24,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="@dimen/l1" android:paddingTop="@dimen/internal_action_bar_size"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}" app:fitsSystemWindowsInsets="top|bottom"
android:paddingEnd="@dimen/l1"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/internal_action_bar_size + (int) @dimen/l1}"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_hide_md2" tools:listitem="@layout/item_hide_md2"
tools:paddingTop="40dp" /> tools:paddingTop="40dp" />
@@ -41,8 +38,9 @@
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
android:layout_marginStart="@dimen/l1" android:layout_marginStart="@dimen/l1"
android:layout_marginEnd="@dimen/l1" android:layout_marginEnd="@dimen/l1"
android:layout_marginBottom="@{viewModel.insets.bottom + (int) @dimen/l1}" android:layout_marginBottom="@dimen/l1"
app:backgroundTint="?colorSurfaceSurfaceVariant" app:backgroundTint="?colorSurfaceSurfaceVariant"
app:layout_fitsSystemWindowsInsets="bottom"
app:srcCompat="@drawable/ic_search_md2" app:srcCompat="@drawable/ic_search_md2"
app:tint="?colorPrimary" app:tint="?colorPrimary"
tools:layout_marginBottom="64dp" /> tools:layout_marginBottom="64dp" />

View File

@@ -22,8 +22,9 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:fillViewport="true" android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}" android:paddingTop="@dimen/internal_action_bar_size"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l3}" android:paddingBottom="@dimen/l3"
app:fitsSystemWindowsInsets="top|bottom"
tools:layout_marginTop="24dp"> tools:layout_marginTop="24dp">
<LinearLayout <LinearLayout

View File

@@ -20,8 +20,9 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:fillViewport="true" android:fillViewport="true"
android:paddingTop="@{viewModel.insets.top + (int) @dimen/internal_action_bar_size}" android:paddingTop="@dimen/internal_action_bar_size"
android:paddingBottom="@{viewModel.insets.bottom + (int) @dimen/l2}" android:paddingBottom="@dimen/l2"
app:fitsSystemWindowsInsets="top|bottom"
tools:paddingTop="24dp"> tools:paddingTop="24dp">
<FrameLayout <FrameLayout
@@ -34,7 +35,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="@dimen/l1"> android:paddingTop="@dimen/l_50">
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
style="@style/WidgetFoundation.Card" style="@style/WidgetFoundation.Card"
@@ -306,6 +307,23 @@
</LinearLayout> </LinearLayout>
<LinearLayout
goneUnless="@{viewModel.loadFailed}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/download_file_error"
android:textAppearance="@style/AppearanceFoundation.Title" />
</LinearLayout>
</FrameLayout> </FrameLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

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