Compare commits

...

453 Commits

Author SHA1 Message Date
Dmitry Val'd
13bf1b27b4 Update strings.xml
Added new lines from original
2017-10-15 03:15:39 +08:00
topjohnwu
f742bb1c47 Hot fix for detecting MagiskHide 2017-10-15 03:12:13 +08:00
topjohnwu
aa0b9e2db2 Bump version 2017-10-14 04:18:14 +08:00
topjohnwu
c10076f7ed Remove debug logs 2017-10-14 04:05:41 +08:00
topjohnwu
bcd92499f2 Massive improvement on Online Repo fetching 2017-10-14 04:05:41 +08:00
topjohnwu
b2bb0d4f72 Fix some external storage permission issues 2017-10-14 00:36:10 +08:00
topjohnwu
e140481f14 Wrap wrapper with buffer 2017-10-13 20:47:14 +08:00
topjohnwu
186bd11463 Reconnect until we got content length 2017-10-13 03:25:56 +08:00
topjohnwu
a0490d6687 Update Trad. Chinese translation 2017-10-13 03:10:35 +08:00
killer7mod
beef740ade update strings.xml for PT-BR 2017-10-13 02:45:02 +08:00
Frieder Bluemle
2ac7786a90 Update commonmark to 0.10.0 2017-10-13 02:44:42 +08:00
Frieder Bluemle
a3fb5e910f Update bouncycastle libs to 1.58 2017-10-13 02:44:42 +08:00
Frieder Bluemle
319afe86b5 Update Gradle wrapper to 4.2.1 2017-10-13 02:44:42 +08:00
Frieder Bluemle
762ab66b86 Fix Lint errors 2017-10-13 02:44:42 +08:00
topjohnwu
0c239a42de Allow secondary users to control Superuser settings except multiuser options 2017-10-13 02:41:43 +08:00
dark-basic #DarkBasic BasicHD
e9322fba26 Update strings.xml
New Lines Added
2017-10-07 23:44:10 +08:00
RoySchutte
39b6df27b3 Update strings.xml 2017-10-07 20:55:00 +08:00
topjohnwu
b1ee284e7f Rename resource -> common 2017-10-07 20:48:45 +08:00
topjohnwu
e986332bf2 Several small snet fixes 2017-10-07 20:47:44 +08:00
topjohnwu
48f9b27381 Seperate JarSigner and add task for host 2017-10-07 20:31:49 +08:00
topjohnwu
42a6e0dd10 Seperate Google proprietary code 2017-10-07 17:12:36 +08:00
topjohnwu
d4798b02ac Move functions 2017-10-04 22:27:14 +08:00
topjohnwu
963edfe8ab Add InputStream mode for signing zips 2017-10-04 22:09:59 +08:00
topjohnwu
53237f3ae0 Update Android Studio and Proguard configs 2017-10-04 15:23:08 +08:00
topjohnwu
64da9281a4 Show progress while downloading modules 2017-10-01 02:38:25 +08:00
topjohnwu
ab7fd9799d Remove cache module exception 2017-10-01 01:38:25 +08:00
topjohnwu
f6bcc84251 Improve repo fetching 2017-10-01 01:28:50 +08:00
topjohnwu
35dc3d9df9 Update WebService 2017-10-01 01:12:45 +08:00
topjohnwu
566714a75d Use override functions 2017-09-30 03:25:50 +08:00
topjohnwu
c92f30b122 Re-organize classes 2017-09-30 03:04:23 +08:00
topjohnwu
294ad094c4 Show repo loading progress by showing repos already loaded 2017-09-30 01:15:34 +08:00
topjohnwu
c1a0f520f9 Prevent flash screen close when tapping outside 2017-09-29 13:20:34 +08:00
topjohnwu
773c24b7fc Bump version 2017-09-28 03:55:53 +08:00
topjohnwu
8f926c7ca9 Load scripts in memory 2017-09-28 03:33:56 +08:00
topjohnwu
c562cbc2bb Update zip and magisk installation 2017-09-26 20:46:58 +08:00
topjohnwu
3fbbb0865a Update trad. Chinese 2017-09-26 02:13:39 +08:00
Naboleo
7d5f612a48 Update strings.xml 2017-09-26 03:07:55 +09:00
linar10
4a5a36440b Update strings.xml 2017-09-26 03:07:41 +09:00
Dmitry Val'd
43dd5cfea1 Update RU translation
Added new or missing lines
2017-09-26 03:07:33 +09:00
dark-basic #DarkBasic BasicHD
7b5fec1842 Update strings.xml 2017-09-26 03:07:20 +09:00
topjohnwu
5762ded601 Properly detect hosts file 2017-09-25 17:55:40 +08:00
topjohnwu
a3abb86daa Only place files in de on FDE enabled devices 2017-09-24 21:29:01 +08:00
topjohnwu
4f5c656b05 Update uninstall method 2017-09-16 03:53:13 +08:00
topjohnwu
a31cddbe7b Prevent NPE 2017-09-16 02:41:24 +08:00
topjohnwu
b4ecd93f1c Proper FBE support: place files in DE 2017-09-15 18:03:25 +08:00
topjohnwu
0acc23e058 Allow dialog to popup 2017-09-15 13:55:36 +08:00
topjohnwu
cdd5f9b628 Fix busybox installation 2017-09-15 13:34:53 +08:00
topjohnwu
4c9f5f4655 Support patching second slot 2017-09-15 13:03:10 +08:00
topjohnwu
b80ba13cb4 Fix strings 2017-09-15 03:47:18 +08:00
Santiago Pintos
8260bdc09c Update translations into spanish
Add two strings: "zip_download_title" and "zip_download_msg"
2017-09-13 10:12:13 -05:00
RoySchutte
24f856e02b Update strings.xml 2017-09-13 10:12:03 -05:00
Mevlüt TOPÇU
3aa619b928 Update
Merge please

Thank you
2017-09-13 10:11:53 -05:00
Taras Korzhak
4cb5e98d94 Update Ukrainian translation 2017-09-13 10:11:25 -05:00
Primokorn
272910575e Update FR strings.xml
Stupid typo
Unhide Magisk Manager should not be translated
2017-09-13 10:09:37 -05:00
topjohnwu
a15a62f4bc Move logic to external script file 2017-09-13 23:07:59 +08:00
topjohnwu
53cf11db8c Fix failure if MagiskManager folder doesn't exist 2017-09-13 23:07:59 +08:00
Dmitry Val'd
01052fbe47 Update strings.xml 2017-09-07 10:45:27 +08:00
dark-basic #DarkBasic BasicHD
a5e1e075c7 Update Strings (6-9-17)
Small Update
New Line Added.
2017-09-07 10:45:12 +08:00
c727
6be32ac688 update german strings
small improvements for new strings
also unified some strings

@topjohnwu:
what do you thing about calling the hidden Magsik Manager also "Magisk Manager" instead of "Unhide Magisk Manager"
The hidden status could be symbolized by an incognito style version of the app icon
advantages:
-same position in app drawer
-no need to translate it
2017-09-07 10:45:02 +08:00
topjohnwu
b362c0ef38 Bump version 2017-09-06 23:06:18 +08:00
topjohnwu
bba9969e31 Fix install button hiding 2017-09-06 23:05:51 +08:00
Primokorn
007ba24809 Update FR strings.xml 2017-09-06 22:33:04 +08:00
topjohnwu
df21539311 Some versioning fixes 2017-09-06 22:32:40 +08:00
topjohnwu
2592cb6019 Show Install button after update check done 2017-09-06 16:28:24 +08:00
topjohnwu
f7df17a7ed Small fix 2017-09-06 15:42:45 +08:00
dark-basic #DarkBasic BasicHD
62f42b72f8 Update Strings.xml (05-09-17)
New lines added.
2017-09-06 14:42:22 +08:00
topjohnwu
a1ba4fda6f Improve install Magisk 2017-09-06 14:41:59 +08:00
topjohnwu
1c06b04c45 Use GNU tar format 2017-09-06 13:39:29 +08:00
topjohnwu
2ee22fd374 Add restore stock image feature 2017-09-05 17:43:13 +08:00
topjohnwu
4c230d9e61 Root shell workaround 2017-09-05 13:46:54 +08:00
topjohnwu
727294fbbe Disable D8, dex not compatible with Android 5.0 2017-09-05 02:57:30 +08:00
Dmitry Val'd
478c43969b Update strings.xml
Added missing/new lines
2017-09-05 02:50:36 +08:00
Jens Lody
79b5303350 Update german translation 2017-09-05 02:50:20 +08:00
topjohnwu
ce4b742b25 Support .img.tar as input 2017-09-04 01:57:45 +08:00
topjohnwu
a9dc15bda5 Update TW translations 2017-09-04 01:14:38 +08:00
topjohnwu
ba6387ff5c Resource cleanup! 2017-09-04 00:58:39 +08:00
linar10
8fa98508b7 Update strings.xml 2017-09-03 23:18:12 +08:00
Dmitry Val'd
decdbaecf9 Update strings.xml
Added missing lines
2017-09-03 23:18:02 +08:00
gh2923
6d87cf9be0 Update Simplified Chinese Translation 2017-09-03 23:17:53 +08:00
Leonidas P
94f434c4a6 Translate Update Channel Strings 2017-09-03 23:17:36 +08:00
dark-basic #DarkBasic BasicHD
7ba867c30b Update Strings - (New Update 03-09-17) 2017-09-03 23:17:18 +08:00
topjohnwu
3424395e10 Calculate offset for unhide 2017-09-03 23:00:54 +08:00
topjohnwu
926c7359a2 Merge download and process repo modules 2017-09-03 22:10:54 +08:00
topjohnwu
ec0af99a2e Fix locale settings 2017-09-03 21:12:09 +08:00
topjohnwu
b4d948886c Fix unzip issues 2017-09-03 21:05:57 +08:00
topjohnwu
4d8d79372a Update strings 2017-09-03 18:28:46 +08:00
topjohnwu
04a589722c Support .img.tar format for ODIN 2017-09-03 17:46:00 +08:00
topjohnwu
d4a10e2873 Various adjustments 2017-09-03 17:46:00 +08:00
topjohnwu
4998ad6c7e Show Manager updates in dialogs 2017-09-03 14:58:21 +08:00
topjohnwu
a07ca5ff50 Slightly change busybox handling 2017-09-03 03:26:01 +08:00
topjohnwu
f07e7571ab Change block detection method 2017-09-03 02:45:43 +08:00
topjohnwu
834c16485c Reduce unnecessary code 2017-09-03 02:34:23 +08:00
topjohnwu
04a4265ef3 Show correct message 2017-09-03 00:17:42 +08:00
topjohnwu
0ec473195d Update install Magisk method 2017-09-03 00:10:14 +08:00
topjohnwu
0bf09256b0 Update Android Studio and Gradle 2017-09-02 19:12:03 +08:00
topjohnwu
db8fd2c913 Add boot image file patch 2017-08-31 03:07:33 +08:00
topjohnwu
dbe6e5b3d7 Simplify app startup 2017-08-30 02:28:24 +08:00
topjohnwu
cc81cd446b Extract ExpandableView code into interface 2017-08-29 04:10:04 +08:00
topjohnwu
439c7118f1 Proper runtime permission implementation 2017-08-29 03:08:09 +08:00
topjohnwu
d8154a5815 Update deprecate code 2017-08-29 01:56:43 +08:00
topjohnwu
4e3787bc0d Add beta update channel 2017-08-29 01:34:42 +08:00
topjohnwu
02e0955924 Fix settings crash 2017-08-29 00:37:52 +08:00
topjohnwu
a78950e822 Reduce boilerplate 2017-08-28 00:27:10 +08:00
topjohnwu
1ce1a94a35 Update translations 2017-08-27 01:38:05 +08:00
gh2923
977b6d9f67 Update Simplified Chinese Translation 2017-08-27 01:09:49 +08:00
Igor Sorocean
b5e6dbd797 update romanian translation 2017-08-27 01:09:41 +08:00
Taras
833e6688f1 Added Ukrainian translation 2017-08-27 01:09:33 +08:00
Dmitry Val'd
bc22c9f84f Update strings.xml
Added missing strings
2017-08-27 01:08:25 +08:00
Mevlüt TOPÇU
2149a7d116 Update
Merge please
2017-08-27 01:08:14 +08:00
dark-basic #DarkBasic BasicHD
29175d2c17 Update Strings.xml 2017-08-27 01:07:45 +08:00
Leonidas P
803454d5c8 Update Greek Strings 2017-08-27 01:07:26 +08:00
topjohnwu
36cf32dc42 Change unhide app temp location 2017-08-27 01:04:55 +08:00
topjohnwu
657f4ab303 Add hide Magisk Manager feature 2017-08-22 03:01:54 +08:00
topjohnwu
ea6552615d Bump version 2017-08-13 01:50:20 +08:00
Generator
4bf3287fce update pt_PT 2017-08-13 01:20:04 +08:00
Mevlüt TOPÇU
832c2034c2 Update
Hi,

Update, translations and typo fix

Merge please

Thank you
2017-08-13 01:19:48 +08:00
RJ Trujillo
b0aa26e1f1 More string updates
* A few grammatical corrections were made
* Everything looks cleaner now
2017-08-13 01:19:27 +08:00
dark-basic #DarkBasic BasicHD
e52baeb967 Update Strings.xml 2017-08-13 01:19:15 +08:00
Leonidas P
8268eb9a83 Update strings.xml 2017-08-13 01:18:55 +08:00
topjohnwu
3cc458abd9 Always use global mount namespace 2017-08-12 17:07:28 +08:00
topjohnwu
337b4c4268 Upgrade Android Studio 2017-08-12 15:54:14 +08:00
topjohnwu
001f8657f6 Use global Magisk native busybox for Magisk Manager 2017-08-12 02:25:55 +08:00
topjohnwu
ea884e7fa1 Re-organize application startup 2017-08-12 01:31:34 +08:00
topjohnwu
1b1394cf5d Improve Markdown support
Close #259
2017-08-08 16:12:49 +08:00
topjohnwu
1eef930dbb Move OnClickListener to Butterknife 2017-08-08 16:09:45 +08:00
topjohnwu
1e175e74ed Prevent crashes 2017-08-07 00:15:46 +08:00
John Wu
75a46c365e Update README.md 2017-08-04 00:23:14 +08:00
topjohnwu
8e7b8825f5 Rename callbackevents to topic/subscribers 2017-08-04 00:17:31 +08:00
topjohnwu
2ecbca303b Update Shell 2017-08-03 23:33:08 +08:00
topjohnwu
8195a4d616 Don't ignore libbusybox.so, we want it removed 2017-08-01 23:54:45 +08:00
topjohnwu
7ba40f925f Remove busybox in APK, download from internet 2017-08-01 23:52:39 +08:00
topjohnwu
345cd1795f Update WebService 2017-08-01 23:08:34 +08:00
topjohnwu
959aaee045 Fix FlashZip crash when fails 2017-07-31 01:19:43 +08:00
topjohnwu
53477f0f59 Improve locale settings 2017-07-31 00:44:38 +08:00
topjohnwu
5716218f41 Update busybox version and bug fixes 2017-07-31 00:21:18 +08:00
topjohnwu
9df6b9d5c0 Remove external files from git
These files should be copied to the correct place by Magisk's build script
2017-07-30 23:17:39 +08:00
topjohnwu
ec46031d36 Update Android Studio 2017-07-30 14:41:22 +08:00
RJ Trujillo
55b84d166a Improve dialog strings
* A space should never follow a question mark or any form of punctuation
* Multiple exclamation marks are not needed
2017-07-30 01:36:25 -05:00
Silvered99
34ae8bacec Update strings.xml 2017-07-30 01:36:16 -05:00
RoySchutte
cb4e5ca0f7 Update strings.xml 2017-07-30 01:36:07 -05:00
Leonidas P
0ba45468c4 Fix typos
these pesky little buggers, you never find them...
2017-07-30 01:35:57 -05:00
Frieder Bluemle
710502784e Update Gradle wrapper to 4.1-rc-1 2017-07-30 01:35:46 -05:00
topjohnwu
0275a8558d Fix locale settings duplicate 2017-07-24 18:37:13 +08:00
topjohnwu
58acc75cf6 Fix SuLog UI 2017-07-24 13:15:05 +08:00
topjohnwu
874ababb9f Fix strings.xml 2017-07-24 02:08:58 +08:00
gh2923
3771e6b0cd Update Simplified Chinese Translation 2017-07-24 01:38:55 +08:00
Sopor
33eaefa966 Add Swedish translation 2017-07-24 01:38:43 +08:00
RoySchutte
cd7e236d57 Update strings.xml 2017-07-24 01:38:18 +08:00
Andrei Conache
54c0b7c7d5 update italian translation 2017-07-24 01:38:02 +08:00
zertyuiop
a2177daec2 Update strings.xml 2017-07-24 01:37:42 +08:00
dark-basic #DarkBasic BasicHD
628386b453 Update Spanish strings.xml 2017-07-24 01:37:23 +08:00
Leonidas P
b222bfb3e0 Update Greek translation 2017-07-24 01:36:09 +08:00
topjohnwu
ab199d883d Change su logs time granularity 2017-07-24 01:26:56 +08:00
topjohnwu
356065d1ee Rewrite SuLogAdapter 2017-07-24 01:26:56 +08:00
topjohnwu
76e7c5623d Simplify ApplicationAdapter filter 2017-07-24 01:26:56 +08:00
topjohnwu
085fba050a Introduce self-written SectionedAdapter 2017-07-24 01:26:45 +08:00
topjohnwu
295334d3ac Preserve toolbar elevation when restart activity 2017-07-23 00:47:54 +08:00
topjohnwu
36124ddca4 Update CallbackEvents 2017-07-23 00:39:38 +08:00
topjohnwu
bd6585765e Add locale settings 2017-07-23 00:33:24 +08:00
topjohnwu
c325deb4ed Random changes 2017-07-22 17:39:34 +08:00
topjohnwu
73bb0b10ee Prevent memory leak in CallbackEvent 2017-07-21 05:18:24 +08:00
topjohnwu
72820b162c Code cleanups 2017-07-21 05:08:39 +08:00
topjohnwu
89e5b8d057 Switch to official BouncyCastle 2017-07-21 03:56:48 +08:00
topjohnwu
da4f53ebbb Don't store multiple repo copies in memory 2017-07-21 02:46:19 +08:00
topjohnwu
8458553b74 Update database helper 2017-07-21 02:10:00 +08:00
topjohnwu
55ecc41d06 Bump version 2017-07-20 03:20:17 +08:00
#DarkBasic - BasicHD
28fcdf2cbb Update strings.xml
Delele Translate "Magisk Modo Sólo Núcleo". (After several hours (Days :v ). I thought it was best left in its original form .Magisk Hide, should also be translated if it were the case, it was better to leave it that way so as not to confuse the users.)
Fix translation error
Translations Updates and added new line
2017-07-20 03:19:58 +08:00
topjohnwu
24087679a8 Update uninstaller 2017-07-20 02:56:36 +08:00
topjohnwu
5ac6a8cb4a Small minor updates 2017-07-20 02:54:34 +08:00
topjohnwu
668d85d14e Improve notification support 2017-07-20 01:44:32 +08:00
topjohnwu
c11a3dc95c Fix Magisk Manager freezing issue 2017-07-20 00:51:30 +08:00
topjohnwu
56f57c20a2 Update AsyncTasks to prevent memory leak 2017-07-19 18:01:22 +08:00
topjohnwu
240d14779a Minor cleanup in check updates 2017-07-19 16:10:17 +08:00
topjohnwu
3550d1e61c Bump version 2017-07-19 00:38:25 +08:00
topjohnwu
6513ad249c Fix string.xml 2017-07-19 00:36:54 +08:00
killer7mod
50297b1880 update strings.xml portuguese brazil 2017-07-19 00:25:36 +08:00
#DarkBasic - BasicHD
f189b78b9e #DBC01 - Translation update 2017-07-19 00:25:23 +08:00
zertyuiop
5c0250f495 Fix too long string
checking_safetyNet_status string is too long.
2017-07-19 00:24:47 +08:00
pavlaras
2093f726e9 Update strings.xml
corrected Greek translation
2017-07-19 00:24:36 +08:00
topjohnwu
10efe3859d Update repo fragment and adapter 2017-07-18 23:18:57 +08:00
topjohnwu
6933bcf7bb Merge shells 2017-07-18 17:14:42 +08:00
topjohnwu
2ea046cd80 Add flashing screen 2017-07-18 17:14:42 +08:00
topjohnwu
f4097a372b Root shell with no outputs 2017-07-18 01:06:05 +08:00
topjohnwu
87ea2a2bef Rewrite root shell 2017-07-16 03:00:01 +08:00
JpegXguy
cc14a1c361 Fix untranslated strings 2017-07-15 01:23:59 +08:00
topjohnwu
bcdface60d Fix crashing when installing modules 2017-07-15 01:22:00 +08:00
topjohnwu
4dc9419d2e Bump version 2017-07-14 02:31:29 +08:00
topjohnwu
d2bcac813e Fix update notifications on Android O 2017-07-14 02:27:02 +08:00
topjohnwu
080c37a7f6 Remove busybox from strings 2017-07-14 01:18:20 +08:00
topjohnwu
f9a3838db6 Fix strings 2017-07-13 15:37:00 +08:00
JpegXguy
1e61db104b Added Greek Language 2017-07-13 15:22:53 +08:00
Generator
30a9c7718d Added (European) Portuguese
Split Portuguese into pt_BR and pt_PT
2017-07-13 15:22:40 +08:00
Dmitry Val'd
34b052b5d3 Update strings.xml
Full and correct translation to russian language
2017-07-13 15:21:27 +08:00
topjohnwu
aaa12853ad Prevent crashing when requesting SN check while checking
Fixed #208, fixed #212
2017-07-13 15:12:43 +08:00
topjohnwu
b0ab55b0bf Only show one notification at a time 2017-07-13 14:51:12 +08:00
topjohnwu
d2f8496f4e Update dependency 2017-07-13 14:47:47 +08:00
topjohnwu
1a69b16d36 Bump version 2017-07-11 01:11:10 +08:00
topjohnwu
b5e8673e62 Fix small UI bug 2017-07-11 01:09:40 +08:00
topjohnwu
264c6a50b6 Update uninstallation 2017-07-11 00:55:53 +08:00
topjohnwu
493642eb38 Minor translation update 2017-07-11 00:55:44 +08:00
gh2923
28d42b9164 fix some expressions 2017-07-08 11:17:41 -05:00
Jens Lody
42f29062ca Fix timeout of temporary granted su-rights. 2017-07-08 11:17:07 -05:00
topjohnwu
c4377ed6c2 Bump version 2017-07-03 01:08:54 +08:00
topjohnwu
7d283ed65f Optimize imports 2017-07-01 18:09:34 +08:00
topjohnwu
bf1f941e50 Adapt to Android O new broadcast limitations 2017-07-01 18:09:34 +08:00
topjohnwu
789fef34ba Fix crash on Android O 2017-07-01 18:09:34 +08:00
topjohnwu
1daf5a611c MagiskHide now defaults to enabled 2017-07-01 17:38:33 +08:00
topjohnwu
6aed1db67e Update Android Studio 2017-07-01 15:57:49 +08:00
gh2923
cf68854770 Update Simplified Chinese Translation 2017-06-20 21:46:36 +08:00
linar10
711392c73b Update Strings PL 2017-06-20 21:45:46 +08:00
c727
9573c32481 update strings.de 2017-06-20 21:45:38 +08:00
RoySchutte
a15f80f79d Create strings.xml 2017-06-20 21:45:28 +08:00
Igor Sorocean
23e7475f06 update romanian translation 2017-06-20 21:45:11 +08:00
topjohnwu
1eb571b787 Proper handle policy changes 2017-06-20 18:33:50 +08:00
topjohnwu
dd3b716d85 Extract expandable viewholder 2017-06-20 17:57:17 +08:00
topjohnwu
28649c07e3 SU policy DB bug fix 2017-06-20 17:57:17 +08:00
topjohnwu
961e02be0d Update Android Studio 2017-06-20 17:54:40 +08:00
topjohnwu
a161491bfd Disable shrinkResources due to buildtool bug 2017-06-16 15:25:22 +08:00
topjohnwu
e0b4d1c1e4 Bump version 2017-06-16 04:07:10 +08:00
topjohnwu
fd4aaab137 Rewrite zip signing 2017-06-16 03:12:57 +08:00
topjohnwu
42d14d5ca2 Update to new build tools, target API 26 2017-06-16 03:06:22 +08:00
topjohnwu
d3ff482c9b Bump version 2017-06-08 22:55:48 +08:00
topjohnwu
f682368eeb Update strings 2017-06-08 22:49:26 +08:00
topjohnwu
4a5d033efb Store data in intent for OTA 2017-06-08 22:35:30 +08:00
topjohnwu
343161b195 Add mount namespace options 2017-06-08 22:27:24 +08:00
topjohnwu
bc576a9659 Update uninstall script 2017-06-08 04:28:55 +08:00
topjohnwu
19e407fcc4 Update translations 2017-06-08 04:23:17 +08:00
RoySchutte
bc7327d004 Update strings.xml 2017-06-08 04:14:12 +08:00
ROBERTO
666fa1c797 Update Italian translation 2017-06-08 04:14:01 +08:00
Igor Sorocean
0eda4a7821 Update romanian translation 2017-06-08 04:13:44 +08:00
topjohnwu
862058fd2b Bump version 2017-06-08 03:20:04 +08:00
topjohnwu
69e5bcd57d Simple OTA implementation 2017-06-07 02:21:58 +08:00
topjohnwu
efeddda328 Use Java synchronize instead serial tasks 2017-06-06 03:21:52 +08:00
topjohnwu
ff6938280e Switch to DB based su configs 2017-06-01 03:18:41 +08:00
RoySchutte
1e4425b30f Update strings-nl.xml 2017-05-31 11:45:02 -05:00
Igor Sorocean
b5d1d8cdad Update romanian translation 2017-05-31 11:44:37 -05:00
gh2923
029be5ccca Update Simplified Chinese Translation 2017-05-31 11:44:17 -05:00
gh2923
29c2d785b5 Update Simplified Chinese Translation 2017-05-31 11:44:04 -05:00
Exalm
abda8cfa32 Updated russian translation 2017-05-31 11:43:48 -05:00
topjohnwu
44e7d79d4c Add Arabic translation
Credits to @xx6600xx
2017-06-01 00:41:36 +08:00
topjohnwu
9a1dc8ee0e Refactor su database 2017-06-01 00:26:36 +08:00
topjohnwu
27879c3f01 Improve Logger 2017-05-31 17:43:55 +08:00
topjohnwu
29096eb5d7 Monitor package (un)install events 2017-05-31 16:31:33 +08:00
topjohnwu
a573baea03 Simplify SU requests, binary should be much superior now 2017-05-30 01:27:10 +08:00
topjohnwu
5af07c4531 Update Traditional Chinese translate 2017-05-28 01:44:29 +08:00
topjohnwu
44e36feb09 Improve multiuser settings and notification 2017-05-28 01:31:19 +08:00
topjohnwu
2a7d996881 Add multiuser support 2017-05-27 02:41:24 +08:00
topjohnwu
738f943a68 Several UI tweaks 2017-05-26 18:20:53 +08:00
dvdandroid
47e62a5681 Small code cleanup 2017-05-24 21:21:15 +02:00
dvdandroid
1ecbfd7590 Adjust theme in about and settings activities 2017-05-24 20:55:47 +02:00
topjohnwu
67c139a04b Fix theme changing glitch 2017-05-24 00:37:15 +08:00
RoySchutte
31cc008249 Update strings.xml
2 small changes to make strings more similar.
2017-05-23 19:47:38 +08:00
topjohnwu
9cb026439d Update translations 2017-05-23 17:02:05 +08:00
topjohnwu
e6f10176c6 Network check 2017-05-23 17:01:38 +08:00
RoySchutte
0917c79470 Update strings.xml
Added and translated new strings.
2017-05-22 23:53:59 +08:00
ROBERTO
597baa986d Updated Italian language 2017-05-22 23:53:43 +08:00
topjohnwu
75cc4b4843 Merge install and status 2017-05-21 12:16:38 +08:00
topjohnwu
aac088d496 Update strings.xml 2017-05-20 03:17:37 +08:00
RoySchutte
a822e5bbc5 Update strings.xml
Fixed many Dutch translations which were gramatically incorrect. Added translations (up-to-date).
Hopefully these translations will make it to the next release, because the current translations aren't pretty *_*.
2017-05-20 03:08:22 +08:00
Igor Sorocean
c527249c21 Add romanian translation 2017-05-20 03:08:14 +08:00
topjohnwu
9ef798f534 Update SafetyNet check UI 2017-05-20 03:04:14 +08:00
topjohnwu
e69b99f089 Update status UI 2017-05-19 08:37:57 -07:00
topjohnwu
55b8079e86 Update MagiskHide method 2017-05-12 23:11:28 +08:00
topjohnwu
e272dbe9af Include busybox binary and remove busybox toggle 2017-05-12 04:05:21 +08:00
topjohnwu
962f8354ac Use new version detection method 2017-05-12 02:25:07 +08:00
topjohnwu
20e4a960f7 Fix strings 2017-05-10 22:54:17 +08:00
ROBERTO
82249cb50a Italian language update 2017-04-28 23:45:41 +08:00
gh2923
fad417e553 Update Simplified Chinese Translation 2017-04-28 15:41:59 +08:00
lindwurm
5ba692f50c l10n: Update Japanese Translations
* Fixed more strings!

Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-04-28 15:41:50 +08:00
topjohnwu
907e01e524 Use stable build tools + retrolambda 2017-04-26 19:04:06 +08:00
lindwurm
b8ed23efa7 l10n: Update Japanese Translations
Signed-off-by: lindwurm <lindwurm.q@gmail.com>
2017-04-26 19:03:14 +08:00
topjohnwu
2b3bbf7e67 Bump version 2017-04-26 00:59:56 +08:00
topjohnwu
464fe627a3 Swap tabs 2017-04-26 00:27:55 +08:00
topjohnwu
6a9e39c470 Support unlimited amount of repos 2017-04-26 00:15:53 +08:00
topjohnwu
7fec9a3cc6 Fix string.xml errors 2017-04-24 22:26:40 +08:00
Primokorn
008f6ef462 Update french strings.xml
Better translation.
2017-04-24 21:54:34 +08:00
lilymaniac
2440c108ca Update values-ko/strings.xml 2017-04-24 21:54:22 +08:00
linar10
430baad8a4 Update strings pl 2017-04-24 21:54:05 +08:00
Nosi
51132e74b4 Changes Spanish 2017-04-24 21:53:48 +08:00
killer7Mod
a4f33e106a Update Portuguese translation 2017-04-24 21:53:21 +08:00
SakuraSa233
baba3190e0 Add Japanese Translation 2017-04-24 21:53:05 +08:00
topjohnwu
47b13aa5ea Use stock FAB; Log monospace; Fixes 2017-04-24 21:52:23 +08:00
topjohnwu
ae88d3054d Finally, official Java 8 support 2017-04-05 17:02:18 +08:00
topjohnwu
411b600e14 Screw that Jack compiler, use retrolambda 2017-03-31 16:04:12 +08:00
topjohnwu
0a0ad9a184 Bump to 4.3.1 2017-03-31 13:17:58 +08:00
topjohnwu
234bead59e Bump version 2017-03-31 06:58:47 +08:00
Primokorn
76de310986 Create french strings.xml
Hope it's not too late for the update :)
2017-03-31 03:23:23 +08:00
topjohnwu
817f050bcd Say goodbye to old modules 2017-03-30 06:52:18 +08:00
topjohnwu
60ae685d1e Change disable to Core Only Mode 2017-03-30 05:16:50 +08:00
Wang Han
4c7bdbb284 Fix crashing when selecting release notes on some devices 2017-03-26 23:55:11 +08:00
topjohnwu
435251ca41 Bump version 2017-03-20 06:24:59 +08:00
topjohnwu
324a0dd38f Update uninstall script 2017-03-20 04:17:04 +08:00
topjohnwu
cc77d93918 Fix string.xml(vi) 2017-03-20 03:38:24 +08:00
Nguyễn Thanh Tài
0ea7d8bd8c Added Vietnamese translation 2017-03-20 03:12:03 +08:00
topjohnwu
849b217143 Fix build issues 2017-03-16 14:08:40 +08:00
Fabio
9af6efba59 Update Italian Translation [2/2] 2017-03-16 13:40:52 +08:00
Fatih Fırıncı
079d6f06ef Added turkish language
Please merge it
2017-03-16 13:40:43 +08:00
gargamelek
9cf0757689 Added czech translation 2017-03-16 13:40:30 +08:00
c727
b54c438948 update strings-de 2017-03-16 13:40:10 +08:00
linar10
c3ff4bfdad Update strings pl 2017-03-16 13:39:49 +08:00
topjohnwu
5d62e066e2 Bump version 2017-02-22 05:06:19 +08:00
topjohnwu
e94219c5a3 Add notification settings 2017-02-22 04:58:03 +08:00
topjohnwu
8ed9634adf Fix Samsung crash 2017-02-22 04:13:21 +08:00
topjohnwu
0aefa9599f Version bump 2017-02-21 03:52:35 +08:00
c727
e279cf0575 update strings-de 2017-02-20 13:40:01 -06:00
topjohnwu
a3f0ef8e77 Many improvements and bug fixes
Close #114
2017-02-21 03:38:37 +08:00
topjohnwu
8eba05ed4a Potentially fix Samsung crash and change colors 2017-02-20 20:11:07 +08:00
topjohnwu
2f78155723 Bump version 2017-02-19 10:49:47 +08:00
topjohnwu
6785221479 Small refinements and bugfixes
Close #109
2017-02-19 10:14:29 +08:00
topjohnwu
9bc410dd3d Add MarkDown styles 2017-02-18 04:35:51 +08:00
gh2923
2491ab6bf9 Update Simplified Chinese Translation 2017-02-17 10:56:44 -06:00
topjohnwu
f615ed40cd Several refinements 2017-02-17 14:07:15 +08:00
linar10
430f2cafc1 Update strings.xml 2017-02-16 23:27:51 -06:00
Deiki-kun
0ad049da88 Updated and corrected Spanish strings.xml 2017-02-16 23:27:39 -06:00
c727
2c7691567b Update strings-de 2017-02-16 23:27:22 -06:00
topjohnwu
1d70d0fe94 Don't show notification again if coming from notification 2017-02-17 09:26:27 +08:00
topjohnwu
ac44f05811 Resource cleanup 2017-02-17 09:03:40 +08:00
topjohnwu
d99252f394 Add update notification 2017-02-17 08:51:51 +08:00
topjohnwu
b58c7ba7c5 Add download button to repo, close #99 2017-02-16 17:50:36 +08:00
topjohnwu
8c5acd1a0a Add traditional Chinese 2017-02-16 17:09:11 +08:00
linar10
b9b1ebf18c Update strings.xml 2017-02-16 01:44:37 -06:00
lilymaniac
8ca132cef0 Add Korean translation
Change-Id: Ie5b9ee02dc179c99b1ff5c50e5ce046cc2f2522e
Signed-off-by: lilymaniac <lilymaniac@outlook.com>
2017-02-16 01:43:46 -06:00
topjohnwu
a03bb90754 Use README.md in details for repo 2017-02-16 05:48:26 +08:00
topjohnwu
d1c939f48a Use temporary files to process zips
Fix #96
2017-02-15 23:46:50 +08:00
gh2923
21b11f1b48 Update Simplified Chinese Translation 2017-02-15 08:44:45 +08:00
topjohnwu
23c84a7803 Massive Zip flashing refactoring 2017-02-15 05:25:24 +08:00
topjohnwu
f9ab060403 Fix su request crashing 2017-02-15 05:07:14 +08:00
topjohnwu
df7a5bf149 Redo styling 2017-02-14 16:35:03 +08:00
topjohnwu
c4afa069df Add custom AlertDialog 2017-02-13 23:11:50 +08:00
topjohnwu
1bfafdb44f Don't reload ApplicationInfo list
Fix #94
2017-02-13 04:00:45 +08:00
topjohnwu
1ef5bd7076 Remove URL in resources 2017-02-13 03:16:39 +08:00
linar10
29176fa4f4 Update strings-pl 2017-02-13 03:14:24 +08:00
topjohnwu
958c95732b Move AboutCardRow to components package 2017-02-13 03:13:24 +08:00
topjohnwu
44b0d4127c Remove GSON and switch to database 2017-02-12 23:27:20 +08:00
topjohnwu
1418ec2416 Remove module helper 2017-02-12 20:53:41 +08:00
topjohnwu
b51978f51c Move asynctasks to seperate package 2017-02-12 19:49:46 +08:00
topjohnwu
b07361580a Contexts are different: Make context clearer 2017-02-12 05:02:18 +08:00
topjohnwu
d1b5ebad7d Several fixes 2017-02-07 07:32:40 +08:00
Exalm
f4ce813de9 Better icon 2017-02-07 06:17:54 +08:00
drbeat
b44ac994d8 fix typos and translate new strings 2017-02-07 06:16:53 +08:00
Exalm
333948814c Russian translation 2017-02-07 06:16:14 +08:00
linar10
1a51ad6e01 Update strings - pl 2017-02-07 06:15:52 +08:00
topjohnwu
22a5c11f0d Fix MagiskHide startup issue 2017-02-07 06:02:06 +08:00
topjohnwu
51b22d1ad4 Make callback events non-static 2017-02-07 04:09:49 +08:00
topjohnwu
bef5969580 No more static crap :) 2017-02-07 02:01:32 +08:00
topjohnwu
c6bf7bb9cd Bump version 2017-02-06 08:34:55 +08:00
linar10
2a84d92cbf Update strings.xml 2017-02-06 08:32:20 +08:00
linar10
62de36b0da Update strings - pl last changes 2017-02-06 08:32:20 +08:00
c727
03a9aaeff7 strings-de latest changes for release 2017-02-06 08:32:07 +08:00
topjohnwu
45765e292d Final fixes 2017-02-06 08:16:48 +08:00
topjohnwu
6e28a26015 Add uninstall button 2017-02-06 03:20:17 +08:00
topjohnwu
9150bf720d Add info for MagiskHide when not using MagiskSU
Close #63
2017-02-06 03:20:16 +08:00
topjohnwu
845864679c Allow multi lines
Fix #53
2017-02-05 22:05:44 +08:00
topjohnwu
b3b2149ebb Optimize root shell and startups 2017-02-05 22:02:14 +08:00
topjohnwu
0886dca385 string.xml update 2017-02-05 04:46:59 +08:00
c727
53198ba4a7 update for strings-de 2017-02-05 04:42:31 +08:00
killer7mod
a9652ee1fd update strings.xml PT 2017-02-05 04:42:22 +08:00
gh2923
75caf2f01c Update Simplified Chinese Translation 2017-02-05 04:42:04 +08:00
linar10
65bab2666e Update strings.xml -PL 2017-02-05 04:41:54 +08:00
Fabio
6d93ae399a Update italian Translation [1/2] 2017-02-05 04:41:41 +08:00
topjohnwu
7239c2e31a Update to the latest settings 2017-02-05 04:40:52 +08:00
topjohnwu
43b7ef8110 Add disable, change busybox 2017-02-02 19:19:22 +08:00
topjohnwu
99ef0b8cb4 Handle MagiskHide at boot 2017-02-01 23:54:32 +08:00
topjohnwu
0efb4da0ee Several bug fixes
Fix #57
2017-01-31 03:39:24 +08:00
linar10
ed7920d61e Added missing entries for strings-pl 2017-01-30 20:12:53 +08:00
c727
c0379c8e25 update strings-de to "Add Superuser settings" 2017-01-30 20:11:56 +08:00
tonymanou
00a0e64fdd Prefer List/Map/Set as declaring type over their implementations
Unless your are using a method declared in subclasses of an
interface, it is better to use the interface as declaring type.
One advantage of this is that changing used implementation will
be much simpler (you will have less declarations to edit).
2017-01-30 20:11:17 +08:00
tonymanou
0dc60debea Fix warning about use of API limited to support package 2017-01-30 20:11:17 +08:00
tonymanou
c44ae5888c Optimize map operations 2017-01-30 20:11:17 +08:00
topjohnwu
b9495cd1bb Improve static data management 2017-01-30 20:04:49 +08:00
topjohnwu
bfec381933 Improve su requests 2017-01-30 19:27:00 +08:00
topjohnwu
2dddb8df69 Reset menu every transaction 2017-01-30 01:51:55 +08:00
topjohnwu
d30397e9c0 Let users know why blacklist PoGO and AP... 2017-01-30 01:40:51 +08:00
topjohnwu
d9597549fd Prevent excessive su requests 2017-01-30 00:44:33 +08:00
topjohnwu
13512b4146 Add BootReceiver 2017-01-29 16:52:43 +08:00
topjohnwu
49e546919a Update logs 2017-01-29 16:20:41 +08:00
topjohnwu
586015c2ed Fix ButterKnife issue
https://code.google.com/p/android/issues/detail?id=231597
2017-01-29 10:27:06 +08:00
topjohnwu
4a7e067d1a Use support library 2017-01-29 00:20:43 +08:00
topjohnwu
9bc0b7f183 Update settings 2017-01-28 22:02:33 +08:00
topjohnwu
cd4dfc9861 Add Superuser settings 2017-01-28 06:13:07 +08:00
topjohnwu
09bdbc1224 Revert "Read only the first line instead of loading the whole file"
This reverts commit a5b573eaaa.

The file shall always have one single line, no need to create a new method
2017-01-28 01:25:51 +08:00
tonymanou
978b3a64c5 Remove context reference from recyclerview adapter 2017-01-28 01:25:15 +08:00
tonymanou
651547ef20 Fix raw use of generics warnings 2017-01-28 01:25:15 +08:00
tonymanou
b4d95977d0 Remove redundant XML namespaces 2017-01-28 01:25:15 +08:00
tonymanou
5d8bb897db Separate JNI glue from actual C code, move CMakeLists file 2017-01-28 01:25:15 +08:00
tonymanou
84c8ecb372 Slight improvement for the navigation drawer 2017-01-28 01:25:15 +08:00
tonymanou
61abe5b948 Do not close the whole application in case of error 2017-01-28 01:25:15 +08:00
tonymanou
a5b573eaaa Read only the first line instead of loading the whole file 2017-01-28 01:25:15 +08:00
topjohnwu
cbb32f82eb Add Superuser logging UI 2017-01-28 01:13:28 +08:00
topjohnwu
ca9334b2df Add tabs to log fragment 2017-01-27 03:43:37 +08:00
topjohnwu
959ed7f866 Implement logging and bug fixes 2017-01-27 01:02:40 +08:00
c727
a5c0411be0 update strings-de 2017-01-26 14:28:27 +08:00
linar10
32e1303742 Add Polish translate 2017-01-26 14:28:04 +08:00
topjohnwu
7263b6fe89 Handle bootblock detect failure cases 2017-01-26 14:25:12 +08:00
topjohnwu
46a4070f84 Prevent shell response crashes 2017-01-26 13:46:54 +08:00
topjohnwu
c3c155a1ed Improved settings 2017-01-26 04:17:51 +08:00
topjohnwu
b067105660 Fix bug where no info is available 2017-01-26 03:45:05 +08:00
topjohnwu
15ca18848e Add su revoke 2017-01-26 03:30:12 +08:00
topjohnwu
67c9e2ead6 Add Superuser management UI 2017-01-26 01:13:23 +08:00
topjohnwu
3681177be4 Rename fragment layouts 2017-01-25 17:07:23 +08:00
topjohnwu
6eb814ef0b Fix some small issues 2017-01-25 16:45:55 +08:00
topjohnwu
bcc695234c Seperate Configs 2017-01-25 13:17:33 +08:00
topjohnwu
ad16a6fc1b Project restructure 2017-01-25 04:33:22 +08:00
topjohnwu
478b7eeb65 Stop countdown when user reacts 2017-01-25 02:16:36 +08:00
topjohnwu
151a153dc9 Fix toasts and timeouts 2017-01-25 01:23:41 +08:00
topjohnwu
ad131854ca Update request popup UI 2017-01-25 01:01:12 +08:00
topjohnwu
0bd0eb9e59 Magisk Manager is now a SU client
1. Add request popup
2. Add su request notifications
3. Add su database helpers
2017-01-24 14:19:28 +08:00
c727
cf16fd0104 update strings-de for Magisk Manager 3.1 2017-01-15 02:37:58 +08:00
tonymanou
21b00ac6ca Use try-with-resources in some places 2017-01-15 02:37:40 +08:00
tonymanou
57e6f3080c Fix generic type 2017-01-15 02:37:40 +08:00
tonymanou
89744100ce Remove unnecessary Butterknife binding in adapters 2017-01-15 02:37:40 +08:00
tonymanou
a718f9bbfd Unbind Butterknife-injected views in fragment's onDestroyView() 2017-01-15 02:37:40 +08:00
tonymanou
e81bc4f044 Clean up main activity code
No need to catch IllegalStateException as we display the fragment from
onCreate() without delay.
2017-01-15 02:37:40 +08:00
tonymanou
4dbacd79ae Matching event [un]registering, call super at the end of onPause/onDestroy
Event unregistered in onDestroy() should be registered in onCreate() to
avoid being registered multiple times.
2017-01-15 02:37:40 +08:00
tonymanou
ae74d54451 Events should be final in order to work 2017-01-15 02:37:40 +08:00
tonymanou
dc316c5669 Set fragment title and [un]register callbacks in onStart/onStop
onStart() is called when the fragment is made visible, whereas onPause()
is called when the fragment looses focus e.g. if a dialog is shown.
Thus:
- there is no need to set the activity's title everytime the fragment
regains focus,
- it is better to listen to event tasks and refresh the state of the UI
while the fragment is actually visible, listening to events until the
fragment is destroyed is useless: if an event is received between
onStop() and onDestroy(), there will be some processing but nothing will
be shown because the fragment is no longer visible.
2017-01-15 02:37:40 +08:00
tonymanou
e9f04256c9 setHasOptionsMenu() should be called from fragment's onCreate() 2017-01-15 02:37:40 +08:00
topjohnwu
e1aabd70e8 Bump version 3.1 2017-01-11 20:31:42 +08:00
topjohnwu
a9dc1b32e0 Add release notes to install button 2017-01-11 19:18:27 +08:00
topjohnwu
01d847ae4e Improve settings 2017-01-11 19:10:30 +08:00
topjohnwu
61e2c3444a Remove token, use ETag to prevent multiple queries 2017-01-11 17:37:35 +08:00
killer7Mod
5363b0f810 updates for portuguese translation 2017-01-11 15:21:58 +08:00
tonymanou
f0e1a8823e Simplify listeners containing async tasks 2017-01-11 15:20:51 +08:00
tonymanou
7be5937aa0 Using checked state listener instead of click listener 2017-01-11 15:20:51 +08:00
tonymanou
8f43055b0e Fix possible list items displaying wrong information
It is better to display empty strings rather than forget to reset
textviews when a viewholder is reused!
2017-01-11 15:20:51 +08:00
tonymanou
953a81b299 Extract getItem() method from onBindViewHolder() 2017-01-11 15:20:51 +08:00
tonymanou
1d34ae7934 Avoid storing context in adapter, static viewholder, remove useless code 2017-01-11 15:20:51 +08:00
tonymanou
2cabb2666b Avoid possible NPE 2017-01-11 15:20:51 +08:00
tonymanou
0b59bb1a29 Do not let magisk hide's apps list blink 2017-01-11 15:17:49 +08:00
tonymanou
c1e7d74b96 Reapply filter when reloading app list 2017-01-11 15:17:49 +08:00
tonymanou
cc262d6595 Change click listener to checked state listener in magisk hide 2017-01-11 15:17:49 +08:00
tonymanou
61d43b118b Use stricter package name test in magisk hide 2017-01-11 15:17:49 +08:00
tonymanou
989d8181dd Do not store context in magisk hide adapter, remove unused code 2017-01-11 15:17:49 +08:00
tonymanou
cffc157d98 Remove useless mView field from fragments 2017-01-11 15:17:49 +08:00
tonymanou
2a70619577 Improve magisk hide app list's adapter, better thread safety 2017-01-11 15:17:49 +08:00
tonymanou
b91919bffa Use string.xml committers' name as translators 2017-01-10 23:06:41 +08:00
tonymanou
fb7a4bf880 Remove empty dutch string, fix german spelling mistake 2017-01-10 23:06:41 +08:00
tonymanou
4b41799a90 Use references in string-array resources 2017-01-10 23:06:41 +08:00
topjohnwu
123f39a21b We can see the token through logs anyway, no need to encrypt 2017-01-10 22:56:48 +08:00
topjohnwu
cadab12737 Prevent root tasks if no root access 2017-01-10 22:47:58 +08:00
topjohnwu
742055c43b Various small changes 2017-01-10 22:30:05 +08:00
topjohnwu
fa73b41fa7 Update repo and module item layout 2017-01-07 03:18:47 +08:00
topjohnwu
a474eafe84 Improve installation UI and dialog 2017-01-07 02:46:50 +08:00
topjohnwu
442fcf921c Change SafetyNet check to manual start 2017-01-07 01:19:18 +08:00
topjohnwu
fb0923f3ab Magisk Hide fragment improvements 2017-01-07 00:29:53 +08:00
topjohnwu
5bb943f845 Fix repo expand card issue 2017-01-06 15:33:31 +08:00
topjohnwu
a3109953d0 Update README.md 2017-01-06 10:59:00 +08:00
Ahmed Zahrani
ff266c8c79 Update Arabic translation. 2017-01-06 02:51:34 +08:00
tonymanou
ef2e02098d Use untranslatable string when storing theme
This fixes #30
2017-01-06 02:44:07 +08:00
Wang Han
93598d3a51 Fix download button overlay on repo description when expanded 2017-01-06 02:43:16 +08:00
Wang Han
53aebcfb1e Fix MagiskHide Fragment Crash when freshing 2017-01-06 02:43:16 +08:00
Wang Han
bb2467d2ac Handle Google API Connection Problems 2017-01-06 02:43:16 +08:00
gh2923
05c063b61d Update Simplified Chinese Translation 2017-01-06 02:38:09 +08:00
209 changed files with 13928 additions and 5402 deletions

6
.gitignore vendored
View File

@@ -3,6 +3,10 @@
/local.properties
.idea/
/build
app/app-release.apk
app/release
*.hprof
app/.externalNativeBuild/
*.sh
public.certificate.x509.pem
private.key.pk8
*.apk

View File

@@ -1,4 +1,7 @@
# Magisk Manager
The project can only be compiled on Android Studio Version 2.2.0+
I use Java 8 features, which requires Jack compiler and that's only available in 2.2.0+
Also, you need to install CMake and NDK to build the zipadjust library for zip preprocessing
This is one of the submodules used in Magisk. The project is licensed under GPL v3 (or newer).
More info are written in the [Magisk Main Repo](https://github.com/topjohnwu/Magisk)
## Building Notes
You need to install CMake and NDK to build the zipadjust library.
There are several files required to let Magisk Manager work properly, and they can be copied by using the build script in the [Magisk Main Repo](https://github.com/topjohnwu/Magisk). These files are: `magisk_uninstaller.sh`, `util_functions.sh`, `public.certificate.x509.pem`, and `private.key.pk8` under the `app/src/main/assets` folder.

View File

@@ -1,5 +0,0 @@
cmake_minimum_required(VERSION 3.6)
add_library(zipadjust SHARED src/main/jni/zipadjust.c)
find_library(libz z)
find_library(liblog log)
target_link_libraries(zipadjust ${libz} ${liblog})

View File

@@ -1,31 +1,30 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdkVersion 26
buildToolsVersion "26.0.2"
defaultConfig {
applicationId "com.topjohnwu.magisk"
minSdkVersion 21
targetSdkVersion 25
versionCode 11
versionName "3.0"
jackOptions {
enabled true
}
targetSdkVersion 26
versionCode 57
versionName "5.4.0"
ndk {
moduleName 'zipadjust'
abiFilters 'x86', 'x86_64', 'armeabi', 'arm64-v8a'
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
}
javaCompileOptions {
annotationProcessorOptions {
argument('butterknife.debuggable', 'false')
}
}
}
compileOptions {
incremental false
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@@ -34,31 +33,34 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
dexOptions {
preDexLibraries = false
preDexLibraries true
javaMaxHeapSize "2g"
}
externalNativeBuild {
cmake {
path 'CMakeLists.txt'
path 'src/main/jni/CMakeLists.txt'
}
}
lintOptions {
disable 'MissingTranslation'
}
}
repositories {
jcenter()
maven { url "https://jitpack.io" }
maven { url "https://maven.google.com" }
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:recyclerview-v7:25.1.0'
compile 'com.android.support:cardview-v7:25.1.0'
compile 'com.android.support:design:25.1.0'
compile 'com.jakewharton:butterknife:8.4.0'
compile 'com.google.code.gson:gson:2.8.0'
compile 'com.github.clans:fab:1.6.4'
compile 'com.madgag.spongycastle:core:1.54.0.0'
compile 'com.madgag.spongycastle:prov:1.54.0.0'
compile 'com.madgag.spongycastle:pkix:1.54.0.0'
compile 'com.madgag.spongycastle:pg:1.54.0.0'
compile 'com.google.android.gms:play-services-safetynet:9.0.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation project(':common')
implementation project(':jarsigner')
implementation 'com.android.support:recyclerview-v7:26.1.0'
implementation 'com.android.support:cardview-v7:26.1.0'
implementation 'com.android.support:design:26.1.0'
implementation 'com.android.support:support-v4:26.1.0'
implementation 'com.jakewharton:butterknife:8.8.1'
implementation 'com.atlassian.commonmark:commonmark:0.10.0'
implementation 'org.kamranzafar:jtar:2.3'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
}

View File

@@ -16,31 +16,9 @@
# public *;
#}
# Gson uses generic type information stored in a class file when working with fields. Proguard
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# Keep all names, we are open source anyway :)
-keepnames class ** { *; }
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.** { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.topjohnwu.magisk.module.** { *; }
-keep class com.topjohnwu.magisk.utils.ModuleHelper$ValueSortedMap { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
-keep class android.support.v7.internal.** { *; }
-keep interface android.support.v7.internal.** { *; }
-keep class android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }
# SpongyCastle
-keep class org.spongycastle.** {*;}
# BouncyCastle
-keep class org.bouncycastle.jcajce.provider.** { *; }
-dontwarn javax.naming.**

View File

@@ -1,29 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.topjohnwu.magisk"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.topjohnwu.magisk">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission
android:name="android.permission.PACKAGE_USAGE_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name=".MagiskManager"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
android:directBootAware="true"
tools:ignore="UnusedAttribute">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:exported="true"/>
android:exported="true" />
<activity
android:name=".SplashActivity"
android:configChanges="orientation|screenSize"
@@ -31,16 +30,55 @@
android:theme="@style/SplashTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".AboutActivity"
android:theme="@style/AppTheme.Transparent"/>
android:theme="@style/AppTheme.Transparent" />
<activity
android:name=".SettingsActivity"
android:theme="@style/AppTheme.Transparent" />
<activity
android:name=".FlashActivity"
android:screenOrientation="nosensor"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/AppTheme.Transparent" />
<activity
android:name=".superuser.RequestActivity"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity="internal.superuser"
android:theme="@android:style/Theme.NoDisplay" />
<activity
android:name=".superuser.SuRequestActivity"
android:excludeFromRecents="true"
android:taskAffinity="internal.superuser"
android:theme="@style/SuRequest" />
<receiver android:name=".superuser.SuReceiver" />
<receiver android:name=".receivers.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".receivers.PackageReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver android:name=".receivers.ManagerUpdate" />
<service android:name=".services.OnBootIntentService" />
<service
android:name=".services.UpdateCheckService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<provider
android:name="android.support.v4.content.FileProvider"
@@ -52,10 +90,11 @@
android:resource="@xml/file_paths" />
</provider>
<!-- Hardcode GMS version -->
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
android:value="11400000" />
</application>
</manifest>
</manifest>

View File

@@ -0,0 +1,276 @@
body {
font-family: Helvetica, arial, sans-serif;
font-size: 14px;
line-height: 1.6;
padding-top: 10px;
padding-bottom: 10px;
background-color: #303030;
color: white;
padding: 15px; }
body > *:first-child {
margin-top: 0 !important; }
body > *:last-child {
margin-bottom: 0 !important; }
a {
color: #4183C4; }
a.absent {
color: #cc0000; }
a.anchor {
display: block;
padding-left: 30px;
margin-left: -30px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0; }
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
cursor: text;
position: relative; }
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
background: url("../../images/modules/styleguide/para.png") no-repeat 10px center;
text-decoration: none; }
h1 tt, h1 code {
font-size: inherit; }
h2 tt, h2 code {
font-size: inherit; }
h3 tt, h3 code {
font-size: inherit; }
h4 tt, h4 code {
font-size: inherit; }
h5 tt, h5 code {
font-size: inherit; }
h6 tt, h6 code {
font-size: inherit; }
h1 {
font-size: 28px; }
h2 {
font-size: 24px;
border-bottom: 1px solid #cccccc; }
h3 {
font-size: 18px; }
h4 {
font-size: 16px; }
h5 {
font-size: 14px; }
h6 {
color: #888888;
font-size: 14px; }
p, blockquote, ul, ol, dl, li, table, pre {
margin: 15px 0; }
hr {
background: transparent url("../../images/modules/pulls/dirty-shade.png") repeat-x 0 0;
border: 0 none;
color: #cccccc;
height: 4px;
padding: 0; }
body > h2:first-child {
margin-top: 0;
padding-top: 0; }
body > h1:first-child {
margin-top: 0;
padding-top: 0; }
body > h1:first-child + h2 {
margin-top: 0;
padding-top: 0; }
body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
margin-top: 0;
padding-top: 0; }
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
margin-top: 0;
padding-top: 0; }
h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
margin-top: 0; }
li p.first {
display: inline-block; }
ul, ol {
padding-left: 30px; }
ul :first-child, ol :first-child {
margin-top: 0; }
ul :last-child, ol :last-child {
margin-bottom: 0; }
dl {
padding: 0; }
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px; }
dl dt:first-child {
padding: 0; }
dl dt > :first-child {
margin-top: 0; }
dl dt > :last-child {
margin-bottom: 0; }
dl dd {
margin: 0 0 15px;
padding: 0 15px; }
dl dd > :first-child {
margin-top: 0; }
dl dd > :last-child {
margin-bottom: 0; }
blockquote {
border-left: 4px solid #404040;
padding: 0 15px;
color: #888888; }
blockquote > :first-child {
margin-top: 0; }
blockquote > :last-child {
margin-bottom: 0; }
table {
padding: 0; }
table tr {
border-top: 1px solid #707070;
background-color: #303030;
margin: 0;
padding: 0; }
table tr:nth-child(2n) {
background-color: #505050; }
table tr th {
font-weight: bold;
border: 1px solid #707070;
text-align: left;
margin: 0;
padding: 6px 13px; }
table tr td {
border: 1px solid #707070;
text-align: left;
margin: 0;
padding: 6px 13px; }
table tr th :first-child, table tr td :first-child {
margin-top: 0; }
table tr th :last-child, table tr td :last-child {
margin-bottom: 0; }
img {
max-width: 100%; }
span.frame {
display: block;
overflow: hidden; }
span.frame > span {
border: 1px solid #dddddd;
display: block;
float: left;
overflow: hidden;
margin: 13px 0 0;
padding: 7px;
width: auto; }
span.frame span img {
display: block;
float: left; }
span.frame span span {
clear: both;
color: #cccccc;
display: block;
padding: 5px 0 0; }
span.align-center {
display: block;
overflow: hidden;
clear: both; }
span.align-center > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: center; }
span.align-center span img {
margin: 0 auto;
text-align: center; }
span.align-right {
display: block;
overflow: hidden;
clear: both; }
span.align-right > span {
display: block;
overflow: hidden;
margin: 13px 0 0;
text-align: right; }
span.align-right span img {
margin: 0;
text-align: right; }
span.float-left {
display: block;
margin-right: 13px;
overflow: hidden;
float: left; }
span.float-left span {
margin: 13px 0 0; }
span.float-right {
display: block;
margin-left: 13px;
overflow: hidden;
float: right; }
span.float-right > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: right; }
code, tt {
margin: 0 2px;
padding: 0 5px;
white-space: nowrap;
border: 1px solid #707070;
background-color: #606060;
border-radius: 3px; }
pre code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent; }
.highlight pre {
background-color: #3f3f3f;
border: 1px solid #707070;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
pre {
background-color: #606060;
border: 1px solid #707070;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
pre code, pre tt {
background-color: transparent;
border: none; }

View File

@@ -0,0 +1,277 @@
body {
font-family: Helvetica, arial, sans-serif;
font-size: 14px;
line-height: 1.6;
padding-top: 10px;
padding-bottom: 10px;
background-color: white;
padding: 15px; }
body > *:first-child {
margin-top: 0 !important; }
body > *:last-child {
margin-bottom: 0 !important; }
a {
color: #4183C4; }
a.absent {
color: #cc0000; }
a.anchor {
display: block;
padding-left: 30px;
margin-left: -30px;
cursor: pointer;
position: absolute;
top: 0;
left: 0;
bottom: 0; }
h1, h2, h3, h4, h5, h6 {
margin: 20px 0 10px;
padding: 0;
font-weight: bold;
-webkit-font-smoothing: antialiased;
cursor: text;
position: relative; }
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
background: url("../../images/modules/styleguide/para.png") no-repeat 10px center;
text-decoration: none; }
h1 tt, h1 code {
font-size: inherit; }
h2 tt, h2 code {
font-size: inherit; }
h3 tt, h3 code {
font-size: inherit; }
h4 tt, h4 code {
font-size: inherit; }
h5 tt, h5 code {
font-size: inherit; }
h6 tt, h6 code {
font-size: inherit; }
h1 {
font-size: 28px;
color: black; }
h2 {
font-size: 24px;
border-bottom: 1px solid #cccccc;
color: black; }
h3 {
font-size: 18px; }
h4 {
font-size: 16px; }
h5 {
font-size: 14px; }
h6 {
color: #777777;
font-size: 14px; }
p, blockquote, ul, ol, dl, li, table, pre {
margin: 15px 0; }
hr {
background: transparent url("../../images/modules/pulls/dirty-shade.png") repeat-x 0 0;
border: 0 none;
color: #cccccc;
height: 4px;
padding: 0; }
body > h2:first-child {
margin-top: 0;
padding-top: 0; }
body > h1:first-child {
margin-top: 0;
padding-top: 0; }
body > h1:first-child + h2 {
margin-top: 0;
padding-top: 0; }
body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
margin-top: 0;
padding-top: 0; }
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
margin-top: 0;
padding-top: 0; }
h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
margin-top: 0; }
li p.first {
display: inline-block; }
ul, ol {
padding-left: 30px; }
ul :first-child, ol :first-child {
margin-top: 0; }
ul :last-child, ol :last-child {
margin-bottom: 0; }
dl {
padding: 0; }
dl dt {
font-size: 14px;
font-weight: bold;
font-style: italic;
padding: 0;
margin: 15px 0 5px; }
dl dt:first-child {
padding: 0; }
dl dt > :first-child {
margin-top: 0; }
dl dt > :last-child {
margin-bottom: 0; }
dl dd {
margin: 0 0 15px;
padding: 0 15px; }
dl dd > :first-child {
margin-top: 0; }
dl dd > :last-child {
margin-bottom: 0; }
blockquote {
border-left: 4px solid #dddddd;
padding: 0 15px;
color: #777777; }
blockquote > :first-child {
margin-top: 0; }
blockquote > :last-child {
margin-bottom: 0; }
table {
padding: 0; }
table tr {
border-top: 1px solid #cccccc;
background-color: white;
margin: 0;
padding: 0; }
table tr:nth-child(2n) {
background-color: #f8f8f8; }
table tr th {
font-weight: bold;
border: 1px solid #cccccc;
text-align: left;
margin: 0;
padding: 6px 13px; }
table tr td {
border: 1px solid #cccccc;
text-align: left;
margin: 0;
padding: 6px 13px; }
table tr th :first-child, table tr td :first-child {
margin-top: 0; }
table tr th :last-child, table tr td :last-child {
margin-bottom: 0; }
img {
max-width: 100%; }
span.frame {
display: block;
overflow: hidden; }
span.frame > span {
border: 1px solid #dddddd;
display: block;
float: left;
overflow: hidden;
margin: 13px 0 0;
padding: 7px;
width: auto; }
span.frame span img {
display: block;
float: left; }
span.frame span span {
clear: both;
color: #333333;
display: block;
padding: 5px 0 0; }
span.align-center {
display: block;
overflow: hidden;
clear: both; }
span.align-center > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: center; }
span.align-center span img {
margin: 0 auto;
text-align: center; }
span.align-right {
display: block;
overflow: hidden;
clear: both; }
span.align-right > span {
display: block;
overflow: hidden;
margin: 13px 0 0;
text-align: right; }
span.align-right span img {
margin: 0;
text-align: right; }
span.float-left {
display: block;
margin-right: 13px;
overflow: hidden;
float: left; }
span.float-left span {
margin: 13px 0 0; }
span.float-right {
display: block;
margin-left: 13px;
overflow: hidden;
float: right; }
span.float-right > span {
display: block;
overflow: hidden;
margin: 13px auto 0;
text-align: right; }
code, tt {
margin: 0 2px;
padding: 0 5px;
white-space: nowrap;
border: 1px solid #eaeaea;
background-color: #f8f8f8;
border-radius: 3px; }
pre code {
margin: 0;
padding: 0;
white-space: pre;
border: none;
background: transparent; }
.highlight pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
pre {
background-color: #f8f8f8;
border: 1px solid #cccccc;
font-size: 13px;
line-height: 19px;
overflow: auto;
padding: 6px 10px;
border-radius: 3px; }
pre code, pre tt {
background-color: transparent;
border: none; }

Binary file not shown.

View File

@@ -1,27 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g
VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE
AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe
Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET
MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G
A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p
ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI
hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM
qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4
wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy
4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU
RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s
zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw
HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ
AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE
CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH
QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG
CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud
EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa
J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y
LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe
+ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX
31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr
sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0=
-----END CERTIFICATE-----

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -3,33 +3,34 @@ package com.topjohnwu.magisk;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.Html;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.components.AboutCardRow;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.components.AlertDialogBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
public class AboutActivity extends AppCompatActivity {
public class AboutActivity extends Activity {
private static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/MagiskManager";
private static final String DONATION_URL = "https://www.paypal.me/topjohnwu";
private static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
private static final String DONATION_URL = "http://topjohnwu.github.io/donate";
private static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/MagiskManager";
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.app_version_info) AboutCardRow appVersionInfo;
@BindView(R.id.app_changelog) AboutCardRow appChangelog;
@@ -38,15 +39,12 @@ public class AboutActivity extends AppCompatActivity {
@BindView(R.id.app_source_code) AboutCardRow appSourceCode;
@BindView(R.id.support_thread) AboutCardRow supportThread;
@BindView(R.id.donation) AboutCardRow donation;
private AlertDialog.Builder builder;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String theme = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("theme", "");
Logger.dev("AboutActivity: Theme is " + theme);
if (theme.equals("Dark")) {
setTheme(R.style.AppTheme_dh);
if (getMagiskManager().isDarkTheme) {
setTheme(R.style.AppTheme_Transparent_Dark);
}
setContentView(R.layout.activity_about);
ButterKnife.bind(this);
@@ -60,27 +58,20 @@ public class AboutActivity extends AppCompatActivity {
ab.setDisplayHomeAsUpEnabled(true);
}
appVersionInfo.setSummary(BuildConfig.VERSION_NAME);
appVersionInfo.setSummary(String.format(Locale.US, "%s (%d)", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
String changes = null;
try {
InputStream is = getAssets().open("changelog.html");
try (InputStream is = getAssets().open("changelog.html")) {
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
changes = new String(buffer);
} catch (IOException ignored) {
}
appChangelog.removeSummary();
if (theme.equals("Dark")) {
builder = new AlertDialog.Builder(this, R.style.AlertDialog_dh);
} else {
builder = new AlertDialog.Builder(this);
}
if (changes == null) {
appChangelog.setVisibility(View.GONE);
} else {
@@ -91,13 +82,11 @@ public class AboutActivity extends AppCompatActivity {
result = Html.fromHtml(changes);
}
appChangelog.setOnClickListener(v -> {
AlertDialog d = builder
AlertDialog d = new AlertDialogBuilder(this)
.setTitle(R.string.app_changelog)
.setMessage(result)
.setPositiveButton(android.R.string.ok, null)
.create();
d.show();
.show();
//noinspection ConstantConditions
((TextView) d.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
@@ -112,7 +101,7 @@ public class AboutActivity extends AppCompatActivity {
} else {
result = Html.fromHtml(getString(R.string.app_developers_));
}
AlertDialog d = builder
AlertDialog d = new AlertDialogBuilder(this)
.setTitle(R.string.app_developers)
.setMessage(result)
.setPositiveButton(android.R.string.ok, null)
@@ -142,18 +131,4 @@ public class AboutActivity extends AppCompatActivity {
setFloating();
}
public void setFloating() {
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
if (isTablet) {
WindowManager.LayoutParams params = getWindow().getAttributes();
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
params.alpha = 1.0f;
params.dimAmount = 0.6f;
params.flags |= 2;
getWindow().setAttributes(params);
setFinishOnTouchOutside(true);
}
}
}

View File

@@ -1,113 +0,0 @@
package com.topjohnwu.magisk;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.v4.view.ViewCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.util.AttributeSet;
import android.view.View;
import com.github.clans.fab.FloatingActionMenu;
import java.util.List;
/**
* Created by Matteo on 08/08/2015.
*
* Floating Action Menu Behavior for Clans.FloatingActionButton
* https://github.com/Clans/FloatingActionButton/
*
* Use this behavior as your app:layout_behavior attribute in your Floating Action Menu to use the
* FabMenu in a Coordinator Layout.
*
* Remember to use the correct namespace for the fab:
* xmlns:fab="http://schemas.android.com/apk/res-auto"
*/
public class FABBehavior extends CoordinatorLayout.Behavior {
private float mTranslationY;
public FABBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
return dependency instanceof Snackbar.SnackbarLayout;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
updateTranslation(parent, child);
}
return false;
}
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
if (dependency instanceof Snackbar.SnackbarLayout) {
revertTranslation(child);
}
}
private void updateTranslation(CoordinatorLayout parent, View child) {
float translationY = getTranslationY(parent, child);
if (translationY != mTranslationY) {
ViewPropertyAnimatorCompat anim = ViewCompat.animate(child);
anim.cancel();
anim.translationY(translationY).setDuration(100);
mTranslationY = translationY;
}
}
private void revertTranslation(View child) {
if (mTranslationY != 0) {
ViewPropertyAnimatorCompat anim = ViewCompat.animate(child);
anim.cancel();
anim.translationY(0).setDuration(100);
mTranslationY = 0;
}
}
private float getTranslationY(CoordinatorLayout parent, View child) {
float minOffset = 0.0F;
List dependencies = parent.getDependencies(child);
int i = 0;
for (int z = dependencies.size(); i < z; ++i) {
View view = (View) dependencies.get(i);
if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(child, view)) {
minOffset = Math.min(minOffset, ViewCompat.getTranslationY(view) - (float) view.getHeight());
}
}
return minOffset;
}
/**
* onStartNestedScroll and onNestedScroll will hide/show the FabMenu when a scroll is detected.
*/
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
View directTargetChild, View target, int nestedScrollAxes) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
nestedScrollAxes);
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
dyUnconsumed);
FloatingActionMenu fabMenu = (FloatingActionMenu) child;
if (dyConsumed > 0 && !fabMenu.isMenuButtonHidden()) {
fabMenu.hideMenuButton(true);
} else if (dyConsumed < 0 && fabMenu.isMenuButtonHidden()) {
fabMenu.showMenuButton(true);
}
}
}

View File

@@ -0,0 +1,153 @@
package com.topjohnwu.magisk;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.topjohnwu.magisk.asyncs.FlashZip;
import com.topjohnwu.magisk.asyncs.InstallMagisk;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.container.AdaptiveList;
import com.topjohnwu.magisk.utils.Shell;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
public class FlashActivity extends Activity {
public static final String SET_ACTION = "action";
public static final String SET_BOOT = "boot";
public static final String SET_ENC = "enc";
public static final String SET_VERITY = "verity";
public static final String FLASH_ZIP = "flash";
public static final String PATCH_BOOT = "patch";
public static final String FLASH_MAGISK = "magisk";
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.flash_logs) RecyclerView flashLogs;
@BindView(R.id.button_panel) LinearLayout buttonPanel;
@BindView(R.id.reboot) Button reboot;
@OnClick(R.id.no_thanks)
public void dismiss() {
finish();
}
@OnClick(R.id.reboot)
public void reboot() {
getShell().su_raw("reboot");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_flash);
ButterKnife.bind(this);
AdaptiveList<String> rootShellOutput = new AdaptiveList<>(flashLogs);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
if (ab != null) {
ab.setTitle(R.string.flashing);
}
setFloating();
setFinishOnTouchOutside(false);
if (!Shell.rootAccess())
reboot.setVisibility(View.GONE);
flashLogs.setAdapter(new FlashLogAdapter(rootShellOutput));
// We must receive a Uri of the target zip
Intent intent = getIntent();
Uri uri = intent.getData();
boolean keepEnc = intent.getBooleanExtra(SET_ENC, false);
boolean keepVerity = intent.getBooleanExtra(SET_VERITY, false);
switch (getIntent().getStringExtra(SET_ACTION)) {
case FLASH_ZIP:
new FlashZip(this, uri, rootShellOutput)
.setCallBack(() -> buttonPanel.setVisibility(View.VISIBLE))
.exec();
break;
case PATCH_BOOT:
new InstallMagisk(this, rootShellOutput, uri, keepEnc, keepVerity, (Uri) intent.getParcelableExtra(SET_BOOT))
.setCallBack(() -> buttonPanel.setVisibility(View.VISIBLE))
.exec();
break;
case FLASH_MAGISK:
String boot = intent.getStringExtra(SET_BOOT);
if (getMagiskManager().remoteMagiskVersionCode < 1370) {
// Use legacy installation method
getShell().su_raw(
"echo \"BOOTIMAGE=" + boot + "\" > /dev/.magisk",
"echo \"KEEPFORCEENCRYPT=" + keepEnc + "\" >> /dev/.magisk",
"echo \"KEEPVERITY=" + keepVerity + "\" >> /dev/.magisk"
);
new FlashZip(this, uri, rootShellOutput)
.setCallBack(() -> buttonPanel.setVisibility(View.VISIBLE))
.exec();
} else {
// Use new installation method
new InstallMagisk(this, rootShellOutput, uri, keepEnc, keepVerity, boot)
.setCallBack(() -> buttonPanel.setVisibility(View.VISIBLE))
.exec();
}
break;
}
}
@Override
public void onBackPressed() {
// Prevent user accidentally press back button
}
private static class FlashLogAdapter extends RecyclerView.Adapter<ViewHolder> {
private List<String> mList;
FlashLogAdapter(List<String> list) {
mList = list;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.list_item_flashlog, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.text.setText(mList.get(position));
}
@Override
public int getItemCount() {
return mList.size();
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.textView) TextView text;
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
}

View File

@@ -1,105 +0,0 @@
package com.topjohnwu.magisk;
import android.app.Fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.CardView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.TextView;
import com.topjohnwu.magisk.receivers.MagiskDlReceiver;
import com.topjohnwu.magisk.utils.CallbackHandler;
import com.topjohnwu.magisk.utils.Utils;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
public class InstallFragment extends Fragment implements CallbackHandler.EventListener {
public static final CallbackHandler.Event blockDetectionDone = new CallbackHandler.Event();
public static List<String> blockList;
public static String bootBlock = null;
@BindView(R.id.install_title) TextView installTitle;
@BindView(R.id.block_spinner) Spinner spinner;
@BindView(R.id.detect_bootimage) Button detectButton;
@BindView(R.id.flash_button) CardView flashButton;
@BindView(R.id.keep_force_enc) CheckBox keepEncChkbox;
@BindView(R.id.keep_verity) CheckBox keepVerityChkbox;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.install_fragment, container, false);
ButterKnife.bind(this, v);
detectButton.setOnClickListener(v1 -> toAutoDetect());
installTitle.setText(getString(R.string.install_magisk_title, StatusFragment.remoteMagiskVersion));
flashButton.setOnClickListener(v1 -> {
String bootImage = bootBlock;
if (bootImage == null) {
bootImage = blockList.get(spinner.getSelectedItemPosition() - 1);
}
String filename = "Magisk-v" + String.valueOf(StatusFragment.remoteMagiskVersion) + ".zip";
String finalBootImage = bootImage;
MainActivity.alertBuilder
.setTitle(getString(R.string.repo_install_title, getString(R.string.magisk)))
.setMessage(getString(R.string.repo_install_msg, filename))
.setCancelable(true)
.setPositiveButton(R.string.download_install, (dialogInterface, i) -> Utils.dlAndReceive(
getActivity(),
new MagiskDlReceiver(finalBootImage, keepEncChkbox.isChecked(), keepVerityChkbox.isChecked()),
StatusFragment.magiskLink,
Utils.getLegalFilename(filename)))
.setNegativeButton(R.string.no_thanks, null)
.show();
});
if (blockDetectionDone.isTriggered) {
updateUI();
}
return v;
}
@Override
public void onTrigger(CallbackHandler.Event event) {
updateUI();
}
private void updateUI() {
List<String> items = new ArrayList<>(blockList);
items.add(0, getString(R.string.auto_detect, bootBlock));
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
toAutoDetect();
}
private void toAutoDetect() {
if (bootBlock != null) {
spinner.setSelection(0);
}
}
@Override
public void onResume() {
super.onResume();
getActivity().setTitle(R.string.install);
CallbackHandler.register(blockDetectionDone, this);
}
@Override
public void onDestroy() {
super.onDestroy();
CallbackHandler.unRegister(blockDetectionDone, this);
}
}

View File

@@ -1,236 +1,55 @@
package com.topjohnwu.magisk;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Calendar;
import java.util.List;
import com.topjohnwu.magisk.adapters.TabFragmentAdapter;
import com.topjohnwu.magisk.components.Fragment;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class LogFragment extends Fragment {
private static final String MAGISK_LOG = "/cache/magisk.log";
private Unbinder unbinder;
@BindView(R.id.txtLog) TextView txtLog;
@BindView(R.id.svLog) ScrollView svLog;
@BindView(R.id.hsvLog) HorizontalScrollView hsvLog;
@BindView(R.id.progressBar) ProgressBar progressBar;
private MenuItem mClickedMenuItem;
@BindView(R.id.container) ViewPager viewPager;
@BindView(R.id.tab) TabLayout tab;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.log_fragment, container, false);
ButterKnife.bind(this, view);
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View v = inflater.inflate(R.layout.fragment_log, container, false);
unbinder = ButterKnife.bind(this, v);
txtLog.setTextIsSelectable(true);
((MainActivity) getActivity()).toolbar.setElevation(0);
new LogManager().read();
TabFragmentAdapter adapter = new TabFragmentAdapter(getChildFragmentManager());
return view;
if (getApplication().isSuClient) {
adapter.addTab(new SuLogFragment(), getString(R.string.superuser));
tab.setupWithViewPager(viewPager);
tab.setVisibility(View.VISIBLE);
}
adapter.addTab(new MagiskLogFragment(), getString(R.string.magisk));
viewPager.setAdapter(adapter);
return v;
}
@Override
public void onResume() {
super.onResume();
setHasOptionsMenu(true);
new LogManager().read();
getActivity().setTitle(R.string.log);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_log, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
mClickedMenuItem = item;
switch (item.getItemId()) {
case R.id.menu_refresh:
new LogManager().read();
return true;
case R.id.menu_send:
new LogManager().send();
return true;
case R.id.menu_save:
new LogManager().save();
return true;
case R.id.menu_clear:
new LogManager().clear();
return true;
default:
return true;
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (mClickedMenuItem != null) {
new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500);
}
} else {
Snackbar.make(txtLog, R.string.permissionNotGranted, Snackbar.LENGTH_LONG).show();
}
}
}
public class LogManager extends Async.RootTask<Object, Void, Object> {
int mode;
File targetFile;
@SuppressLint("DefaultLocale")
@Override
protected Object doInBackground(Object... params) {
mode = (int) params[0];
switch (mode) {
case 0:
List<String> logList = Utils.readFile(MAGISK_LOG);
StringBuilder llog = new StringBuilder(15 * 10 * 1024);
for (String s : logList) {
llog.append(s).append("\n");
}
return llog.toString();
case 1:
Shell.su("echo > " + MAGISK_LOG);
Snackbar.make(txtLog, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
return "";
case 2:
case 3:
if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}
return false;
}
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
return false;
Calendar now = Calendar.getInstance();
String filename = String.format(
"magisk_%s_%04d%02d%02d_%02d%02d%02d.log", "error",
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
targetFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MagiskManager/" + filename);
if ((!targetFile.getParentFile().exists() && !targetFile.getParentFile().mkdirs())
|| (targetFile.exists() && !targetFile.delete()))
return false;
List<String> in = Utils.readFile(MAGISK_LOG);
try {
FileWriter out = new FileWriter(targetFile);
for (String line : in) {
out.write(line + "\n");
}
out.close();
return true;
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
return null;
}
@Override
protected void onPostExecute(Object o) {
boolean bool;
String llog;
switch (mode) {
case 0:
case 1:
llog = (String) o;
progressBar.setVisibility(View.GONE);
if (llog.length() == 0)
txtLog.setText(R.string.log_is_empty);
else
txtLog.setText(llog);
svLog.post(() -> svLog.scrollTo(0, txtLog.getHeight()));
hsvLog.post(() -> hsvLog.scrollTo(0, 0));
break;
case 2:
bool = (boolean) o;
if (bool)
Toast.makeText(getActivity(), targetFile.toString(), Toast.LENGTH_LONG).show();
else
Toast.makeText(getActivity(), getString(R.string.logs_save_failed), Toast.LENGTH_LONG).show();
break;
case 3:
bool = (boolean) o;
if (bool) {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(targetFile));
sendIntent.setType("application/html");
startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend)));
} else {
Toast.makeText(getActivity(), getString(R.string.logs_save_failed), Toast.LENGTH_LONG).show();
}
}
}
public void read() {
exec(0);
}
public void clear() {
exec(1);
}
public void save() {
exec(2);
}
public void send() {
exec(3);
}
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}

View File

@@ -0,0 +1,368 @@
package com.topjohnwu.magisk;
import android.app.NotificationManager;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.CardView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.topjohnwu.magisk.asyncs.CheckSafetyNet;
import com.topjohnwu.magisk.asyncs.CheckUpdates;
import com.topjohnwu.magisk.components.AlertDialogBuilder;
import com.topjohnwu.magisk.components.ExpandableView;
import com.topjohnwu.magisk.components.Fragment;
import com.topjohnwu.magisk.components.SnackbarMaker;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
public class MagiskFragment extends Fragment
implements Topic.Subscriber, SwipeRefreshLayout.OnRefreshListener, ExpandableView {
public static final int CAUSE_SERVICE_DISCONNECTED = 0x00001;
public static final int CAUSE_NETWORK_LOST = 0x00010;
public static final int RESPONSE_ERR = 0x00100;
public static final int BASIC_PASS = 0x01000;
public static final int CTS_PASS = 0x10000;
private Container expandableContainer = new Container();
private MagiskManager mm;
private Unbinder unbinder;
private static boolean shownDialog = false;
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.magisk_update_card) CardView magiskUpdateCard;
@BindView(R.id.magisk_update_icon) ImageView magiskUpdateIcon;
@BindView(R.id.magisk_update_status) TextView magiskUpdateText;
@BindView(R.id.magisk_update_progress) ProgressBar magiskUpdateProgress;
@BindView(R.id.magisk_status_icon) ImageView magiskStatusIcon;
@BindView(R.id.magisk_version) TextView magiskVersionText;
@BindView(R.id.root_status_icon) ImageView rootStatusIcon;
@BindView(R.id.root_status) TextView rootStatusText;
@BindView(R.id.safetyNet_card) CardView safetyNetCard;
@BindView(R.id.safetyNet_refresh) ImageView safetyNetRefreshIcon;
@BindView(R.id.safetyNet_status) TextView safetyNetStatusText;
@BindView(R.id.safetyNet_check_progress) ProgressBar safetyNetProgress;
@BindView(R.id.expand_layout) LinearLayout expandLayout;
@BindView(R.id.cts_status_icon) ImageView ctsStatusIcon;
@BindView(R.id.cts_status) TextView ctsStatusText;
@BindView(R.id.basic_status_icon) ImageView basicStatusIcon;
@BindView(R.id.basic_status) TextView basicStatusText;
@BindView(R.id.bootimage_card) CardView bootImageCard;
@BindView(R.id.block_spinner) Spinner spinner;
@BindView(R.id.detect_bootimage) Button detectButton;
@BindView(R.id.install_option_card) CardView installOptionCard;
@BindView(R.id.keep_force_enc) CheckBox keepEncChkbox;
@BindView(R.id.keep_verity) CheckBox keepVerityChkbox;
@BindView(R.id.install_button) CardView installButton;
@BindView(R.id.install_text) TextView installText;
@BindView(R.id.uninstall_button) CardView uninstallButton;
@BindColor(R.color.red500) int colorBad;
@BindColor(R.color.green500) int colorOK;
@BindColor(R.color.yellow500) int colorWarn;
@BindColor(R.color.grey500) int colorNeutral;
@BindColor(R.color.blue500) int colorInfo;
@OnClick(R.id.safetyNet_title)
void safetyNet() {
Runnable task = () -> {
safetyNetProgress.setVisibility(View.VISIBLE);
safetyNetRefreshIcon.setVisibility(View.GONE);
safetyNetStatusText.setText(R.string.checking_safetyNet_status);
new CheckSafetyNet(getActivity()).exec();
collapse();
};
if (mm.snet_version < 0) {
// Show dialog
new AlertDialogBuilder(getActivity())
.setTitle(R.string.proprietary_title)
.setMessage(R.string.proprietary_notice)
.setCancelable(true)
.setPositiveButton(R.string.yes, (d, i) -> task.run())
.setNegativeButton(R.string.no_thanks, null)
.show();
} else {
task.run();
}
}
@OnClick(R.id.install_button)
void install() {
shownDialog = true;
// Show Manager update first
if (mm.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
Utils.showManagerInstallDialog(getActivity());
return;
}
((NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE)).cancelAll();
Utils.showMagiskInstallDialog(this,
keepEncChkbox.isChecked(), keepVerityChkbox.isChecked());
}
@OnClick(R.id.uninstall_button)
void uninstall() {
Utils.showUninstallDialog(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_magisk, container, false);
unbinder = ButterKnife.bind(this, v);
getActivity().setTitle(R.string.magisk);
mm = getApplication();
expandableContainer.expandLayout = expandLayout;
setupExpandable();
mSwipeRefreshLayout.setOnRefreshListener(this);
updateUI();
return v;
}
@Override
public void onRefresh() {
mm.getMagiskInfo();
updateUI();
magiskUpdateText.setText(R.string.checking_for_updates);
magiskUpdateProgress.setVisibility(View.VISIBLE);
magiskUpdateIcon.setVisibility(View.GONE);
safetyNetStatusText.setText(R.string.safetyNet_check_text);
mm.safetyNetDone.hasPublished = false;
mm.updateCheckDone.hasPublished = false;
mm.remoteMagiskVersionString = null;
mm.remoteMagiskVersionCode = -1;
collapse();
shownDialog = false;
// Trigger state check
if (Utils.checkNetworkStatus(mm)) {
new CheckUpdates(getActivity()).exec();
} else {
mSwipeRefreshLayout.setRefreshing(false);
}
}
@Override
public void onTopicPublished(Topic topic, Object result) {
if (topic == mm.updateCheckDone) {
updateCheckUI();
} else if (topic == mm.safetyNetDone) {
updateSafetyNetUI((int) result);
}
}
@Override
public Topic[] getSubscription() {
return new Topic[] { mm.updateCheckDone, mm.safetyNetDone };
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
@Override
public Container getContainer() {
return expandableContainer;
}
public String getSelectedBootImage() {
if (Shell.rootAccess()) {
if (mm.bootBlock != null) {
return mm.bootBlock;
} else {
int idx = spinner.getSelectedItemPosition();
if (idx > 0) {
return mm.blockList.get(idx - 1);
} else {
SnackbarMaker.make(getActivity(),
R.string.manual_boot_image, Snackbar.LENGTH_LONG).show();
return null;
}
}
} else {
return null;
}
}
private void updateUI() {
((MainActivity) getActivity()).checkHideSection();
boolean hasNetwork = Utils.checkNetworkStatus(getActivity());
boolean hasRoot = Shell.rootAccess();
boolean isUpToDate = mm.magiskVersionCode > 1300;
magiskUpdateCard.setVisibility(hasNetwork ? View.VISIBLE : View.GONE);
safetyNetCard.setVisibility(hasNetwork ? View.VISIBLE : View.GONE);
bootImageCard.setVisibility(hasNetwork && hasRoot ? View.VISIBLE : View.GONE);
installOptionCard.setVisibility(hasNetwork ? View.VISIBLE : View.GONE);
uninstallButton.setVisibility(isUpToDate && hasRoot ? View.VISIBLE : View.GONE);
int image, color;
if (mm.magiskVersionCode < 0) {
color = colorBad;
image = R.drawable.ic_cancel;
magiskVersionText.setText(R.string.magisk_version_error);
} else {
color = colorOK;
image = R.drawable.ic_check_circle;
magiskVersionText.setText(getString(R.string.current_magisk_title, "v" + mm.magiskVersionString));
}
magiskStatusIcon.setImageResource(image);
magiskStatusIcon.setColorFilter(color);
switch (Shell.rootStatus) {
case 0:
color = colorBad;
image = R.drawable.ic_cancel;
rootStatusText.setText(R.string.not_rooted);
break;
case 1:
if (mm.suVersion != null) {
color = colorOK;
image = R.drawable.ic_check_circle;
rootStatusText.setText(mm.suVersion);
break;
}
case -1:
default:
color = colorNeutral;
image = R.drawable.ic_help;
rootStatusText.setText(R.string.root_error);
}
rootStatusIcon.setImageResource(image);
rootStatusIcon.setColorFilter(color);
List<String> items = new ArrayList<>();
if (mm.bootBlock != null) {
items.add(getString(R.string.auto_detect, mm.bootBlock));
spinner.setEnabled(false);
} else {
items.add(getString(R.string.cannot_auto_detect));
if (mm.blockList != null)
items.addAll(mm.blockList);
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
android.R.layout.simple_spinner_item, items);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
}
private void updateCheckUI() {
int image, color;
if (mm.remoteMagiskVersionCode < 0) {
color = colorNeutral;
image = R.drawable.ic_help;
magiskUpdateText.setText(R.string.cannot_check_updates);
} else {
color = colorOK;
image = R.drawable.ic_check_circle;
magiskUpdateText.setText(getString(R.string.install_magisk_title, "v" + mm.remoteMagiskVersionString));
}
installButton.setVisibility(View.VISIBLE);
if (mm.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
installText.setText(getString(R.string.update, getString(R.string.app_name)));
} else if (mm.magiskVersionCode > 0 && mm.remoteMagiskVersionCode > mm.magiskVersionCode) {
installText.setText(getString(R.string.update, getString(R.string.magisk)));
} else {
installText.setText(R.string.install);
}
if (!shownDialog && (mm.remoteMagiskVersionCode > mm.magiskVersionCode
|| mm.remoteManagerVersionCode > BuildConfig.VERSION_CODE)) {
install();
}
magiskUpdateIcon.setImageResource(image);
magiskUpdateIcon.setColorFilter(color);
magiskUpdateIcon.setVisibility(View.VISIBLE);
magiskUpdateProgress.setVisibility(View.GONE);
mSwipeRefreshLayout.setRefreshing(false);
}
private void updateSafetyNetUI(int response) {
safetyNetProgress.setVisibility(View.GONE);
safetyNetRefreshIcon.setVisibility(View.VISIBLE);
if (response < 0) {
safetyNetStatusText.setText(R.string.safetyNet_api_error);
} else if ((response & 0x111) == 0) {
safetyNetStatusText.setText(R.string.safetyNet_check_success);
boolean b;
b = (response & CTS_PASS) != 0;
ctsStatusText.setText("ctsProfile: " + b);
ctsStatusIcon.setImageResource(b ? R.drawable.ic_check_circle : R.drawable.ic_cancel);
ctsStatusIcon.setColorFilter(b ? colorOK : colorBad);
b = (response & BASIC_PASS) != 0;
basicStatusText.setText("basicIntegrity: " + b);
basicStatusIcon.setImageResource(b ? R.drawable.ic_check_circle : R.drawable.ic_cancel);
basicStatusIcon.setColorFilter(b ? colorOK : colorBad);
expand();
} else {
@StringRes int resid;
switch (response) {
case CAUSE_SERVICE_DISCONNECTED:
resid = R.string.safetyNet_network_loss;
break;
case CAUSE_NETWORK_LOST:
resid = R.string.safetyNet_service_disconnected;
break;
case RESPONSE_ERR:
default:
resid = R.string.safetyNet_res_invalid;
break;
}
safetyNetStatusText.setText(resid);
}
}
}

View File

@@ -1,11 +1,7 @@
package com.topjohnwu.magisk;
import android.app.Fragment;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
@@ -16,121 +12,85 @@ import android.view.ViewGroup;
import android.widget.SearchView;
import com.topjohnwu.magisk.adapters.ApplicationAdapter;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.Shell;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.topjohnwu.magisk.components.Fragment;
import com.topjohnwu.magisk.utils.Topic;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class MagiskHideFragment extends Fragment {
public class MagiskHideFragment extends Fragment implements Topic.Subscriber {
private Unbinder unbinder;
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
private PackageManager packageManager;
private View mView;
private List<ApplicationInfo> listApps = new ArrayList<>(), fListApps = new ArrayList<>();
private List<String> hideList = new ArrayList<>();
private ApplicationAdapter appAdapter = new ApplicationAdapter(fListApps, hideList);
private ApplicationAdapter appAdapter;
private SearchView.OnQueryTextListener searchListener;
private String lastFilter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.magisk_hide_fragment, container, false);
ButterKnife.bind(this, mView);
packageManager = getActivity().getPackageManager();
View view = inflater.inflate(R.layout.fragment_magisk_hide, container, false);
unbinder = ButterKnife.bind(this, view);
lastFilter = "";
mSwipeRefreshLayout.setRefreshing(true);
mSwipeRefreshLayout.setOnRefreshListener(() -> {
recyclerView.setVisibility(View.GONE);
new LoadApps().exec();
});
mSwipeRefreshLayout.setOnRefreshListener(() -> appAdapter.refresh());
appAdapter = new ApplicationAdapter(getActivity());
recyclerView.setAdapter(appAdapter);
searchListener = new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
lastFilter = query;
appAdapter.filter(query);
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
new FilterApps().exec(newText);
lastFilter = newText;
appAdapter.filter(newText);
return false;
}
};
new LoadApps().exec();
return mView;
getActivity().setTitle(R.string.magiskhide);
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_magiskhide, menu);
SearchView search = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.app_search));
SearchView search = (SearchView) menu.findItem(R.id.app_search).getActionView();
search.setOnQueryTextListener(searchListener);
}
@Override
public void onResume() {
super.onResume();
setHasOptionsMenu(true);
mView = this.getView();
getActivity().setTitle(R.string.magiskhide);
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
private class LoadApps extends Async.RootTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
listApps.clear();
hideList.clear();
fListApps.clear();
listApps.addAll(packageManager.getInstalledApplications(PackageManager.GET_META_DATA));
Collections.sort(listApps, (a, b) -> a.loadLabel(packageManager).toString().toLowerCase()
.compareTo(b.loadLabel(packageManager).toString().toLowerCase()));
hideList.addAll(Shell.su(Async.MAGISK_HIDE_PATH + "list"));
fListApps.addAll(listApps);
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
updateUI();
}
}
private class FilterApps extends Async.NormalTask<String, Void, Void> {
@Override
protected Void doInBackground(String... strings) {
String newText = strings[0];
fListApps.clear();
for (ApplicationInfo info : listApps) {
if (info.loadLabel(packageManager).toString().toLowerCase().contains(newText.toLowerCase())
|| info.packageName.toLowerCase().contains(newText.toLowerCase())) {
fListApps.add(info);
}
}
return null;
}
@Override
protected void onPostExecute(Void v) {
appAdapter.notifyDataSetChanged();
}
}
private void updateUI() {
appAdapter.notifyDataSetChanged();
recyclerView.setVisibility(View.VISIBLE);
@Override
public void onTopicPublished(Topic topic, Object result) {
mSwipeRefreshLayout.setRefreshing(false);
appAdapter.filter(lastFilter);
}
}
@Override
public Topic[] getSubscription() {
return new Topic[] { getApplication().magiskHideDone };
}
}

View File

@@ -0,0 +1,231 @@
package com.topjohnwu.magisk;
import android.Manifest;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.Nullable;
import android.support.design.widget.Snackbar;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import com.topjohnwu.magisk.asyncs.ParallelTask;
import com.topjohnwu.magisk.components.Fragment;
import com.topjohnwu.magisk.components.SnackbarMaker;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Calendar;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class MagiskLogFragment extends Fragment {
private static final String MAGISK_LOG = "/cache/magisk.log";
private Unbinder unbinder;
@BindView(R.id.txtLog) TextView txtLog;
@BindView(R.id.svLog) ScrollView svLog;
@BindView(R.id.hsvLog) HorizontalScrollView hsvLog;
@BindView(R.id.progressBar) ProgressBar progressBar;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_magisk_log, container, false);
unbinder = ButterKnife.bind(this, view);
setHasOptionsMenu(true);
txtLog.setTextIsSelectable(true);
new LogManager().read();
return view;
}
@Override
public void onStart() {
super.onStart();
getActivity().setTitle(R.string.log);
}
@Override
public void onResume() {
super.onResume();
new LogManager().read();
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_log, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
new LogManager().read();
return true;
case R.id.menu_save:
Utils.runWithPermission(getActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> new LogManager().save());
return true;
case R.id.menu_clear:
new LogManager().clear();
return true;
default:
return true;
}
}
private class LogManager extends ParallelTask<Object, Void, Object> {
private int mode;
private File targetFile;
LogManager() {
super(MagiskLogFragment.this.getActivity());
}
@SuppressLint("DefaultLocale")
@Override
protected Object doInBackground(Object... params) {
mode = (int) params[0];
switch (mode) {
case 0:
StringBuildingList logList = new StringBuildingList();
getShell().su(logList, "cat " + MAGISK_LOG);
return logList.toString();
case 1:
getShell().su_raw("echo -n > " + MAGISK_LOG);
SnackbarMaker.make(txtLog, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
return "";
case 2:
Calendar now = Calendar.getInstance();
String filename = String.format(
"magisk_%s_%04d%02d%02d_%02d%02d%02d.log", "error",
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
targetFile = new File(Environment.getExternalStorageDirectory() + "/MagiskManager/" + filename);
if ((!targetFile.getParentFile().exists() && !targetFile.getParentFile().mkdirs())
|| (targetFile.exists() && !targetFile.delete())) {
return false;
}
try (FileWriter out = new FileWriter(targetFile)) {
FileWritingList fileWritingList = new FileWritingList(out);
getShell().su(fileWritingList, "cat " + MAGISK_LOG);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
return null;
}
@Override
protected void onPostExecute(Object o) {
if (o == null) return;
switch (mode) {
case 0:
case 1:
String llog = (String) o;
progressBar.setVisibility(View.GONE);
if (TextUtils.isEmpty(llog))
txtLog.setText(R.string.log_is_empty);
else
txtLog.setText(llog);
svLog.post(() -> svLog.scrollTo(0, txtLog.getHeight()));
hsvLog.post(() -> hsvLog.scrollTo(0, 0));
break;
case 2:
boolean bool = (boolean) o;
if (bool) {
getMagiskManager().toast(targetFile.toString(), Toast.LENGTH_LONG);
} else {
getMagiskManager().toast(R.string.logs_save_failed, Toast.LENGTH_LONG);
}
break;
}
}
void read() {
exec(0);
}
void clear() {
exec(1);
}
void save() {
exec(2);
}
}
private static class StringBuildingList extends Shell.AbstractList<String> {
StringBuilder builder;
StringBuildingList() {
builder = new StringBuilder();
}
@Override
public boolean add(String s) {
builder.append(s).append("\n");
return true;
}
@Override
public String toString() {
return builder.toString();
}
}
private static class FileWritingList extends Shell.AbstractList<String> {
private FileWriter writer;
FileWritingList(FileWriter out) {
writer = out;
}
@Override
public boolean add(String s) {
try {
writer.write(s + "\n");
} catch (IOException ignored) {}
return true;
}
}
}

View File

@@ -0,0 +1,326 @@
package com.topjohnwu.magisk;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.job.JobInfo;
import android.app.job.JobScheduler;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.widget.Toast;
import com.topjohnwu.magisk.asyncs.CheckUpdates;
import com.topjohnwu.magisk.asyncs.DownloadBusybox;
import com.topjohnwu.magisk.asyncs.LoadModules;
import com.topjohnwu.magisk.asyncs.ParallelTask;
import com.topjohnwu.magisk.asyncs.UpdateRepos;
import com.topjohnwu.magisk.container.Module;
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.database.SuDatabaseHelper;
import com.topjohnwu.magisk.services.UpdateCheckService;
import com.topjohnwu.magisk.superuser.SuReceiver;
import com.topjohnwu.magisk.superuser.SuRequestActivity;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutionException;
public class MagiskManager extends Application {
public static final String MAGISK_DISABLE_FILE = "/cache/.disable_magisk";
public static final String MAGISK_HOST_FILE = "/magisk/.core/hosts";
public static final String TMP_FOLDER_PATH = "/dev/tmp";
public static final String MAGISK_PATH = "/magisk";
public static final String INTENT_SECTION = "section";
public static final String INTENT_VERSION = "version";
public static final String INTENT_LINK = "link";
public static final String MAGISKHIDE_PROP = "persist.magisk.hide";
public static final String DISABLE_INDICATION_PROP = "ro.magisk.disable";
public static final String NOTIFICATION_CHANNEL = "magisk_update_notice";
public static final String BUSYBOXPATH = "/dev/magisk/bin";
public static final int UPDATE_SERVICE_ID = 1;
// Topics
public final Topic magiskHideDone = new Topic();
public final Topic reloadActivity = new Topic();
public final Topic moduleLoadDone = new Topic();
public final Topic repoLoadDone = new Topic();
public final Topic updateCheckDone = new Topic();
public final Topic safetyNetDone = new Topic();
public final Topic localeDone = new Topic();
// Info
public String magiskVersionString;
public int magiskVersionCode = -1;
public String remoteMagiskVersionString;
public int remoteMagiskVersionCode = -1;
public String magiskLink;
public String releaseNoteLink;
public String remoteManagerVersionString;
public int remoteManagerVersionCode = -1;
public String managerLink;
public String bootBlock = null;
public boolean isSuClient = false;
public String suVersion = null;
public boolean disabled;
// Data
public Map<String, Module> moduleMap;
public List<String> blockList;
public List<Locale> locales;
// Configurations
public static Locale locale;
public static Locale defaultLocale;
public boolean magiskHide;
public boolean isDarkTheme;
public boolean updateNotification;
public boolean suReauth;
public int suRequestTimeout;
public int suLogTimeout = 14;
public int suAccessState;
public int multiuserMode;
public int suResponseType;
public int suNotificationType;
public int suNamespaceMode;
public String localeConfig;
public int updateChannel;
public String bootFormat;
public int snet_version;
// Global resources
public SharedPreferences prefs;
public SuDatabaseHelper suDB;
public RepoDatabaseHelper repoDB;
public Shell shell;
private static Handler mHandler = new Handler();
private boolean started = false;
private static class LoadLocale extends ParallelTask<Void, Void, Void> {
LoadLocale(Context context) {
super(context);
}
@Override
protected Void doInBackground(Void... voids) {
getMagiskManager().locales = Utils.getAvailableLocale(getMagiskManager());
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
getMagiskManager().localeDone.publish();
}
}
@Override
public void onCreate() {
super.onCreate();
prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (getDatabasePath(SuDatabaseHelper.DB_NAME).exists()) {
// Don't migrate yet, wait and check Magisk version
suDB = new SuDatabaseHelper(this);
} else {
suDB = new SuDatabaseHelper(Utils.getEncContext(this));
}
repoDB = new RepoDatabaseHelper(this);
defaultLocale = Locale.getDefault();
setLocale();
loadConfig();
}
public void setLocale() {
localeConfig = prefs.getString("locale", "");
if (localeConfig.isEmpty()) {
locale = defaultLocale;
} else {
locale = Locale.forLanguageTag(localeConfig);
}
Resources res = getBaseContext().getResources();
Configuration config = new Configuration(res.getConfiguration());
config.setLocale(locale);
res.updateConfiguration(config, res.getDisplayMetrics());
}
public void loadConfig() {
isDarkTheme = prefs.getBoolean("dark_theme", false);
// su
suRequestTimeout = Utils.getPrefsInt(prefs, "su_request_timeout", 10);
suResponseType = Utils.getPrefsInt(prefs, "su_auto_response", SuRequestActivity.PROMPT);
suNotificationType = Utils.getPrefsInt(prefs, "su_notification", SuReceiver.TOAST);
suReauth = prefs.getBoolean("su_reauth", false);
suAccessState = suDB.getSettings(SuDatabaseHelper.ROOT_ACCESS, SuDatabaseHelper.ROOT_ACCESS_APPS_AND_ADB);
multiuserMode = suDB.getSettings(SuDatabaseHelper.MULTIUSER_MODE, SuDatabaseHelper.MULTIUSER_MODE_OWNER_ONLY);
suNamespaceMode = suDB.getSettings(SuDatabaseHelper.MNT_NS, SuDatabaseHelper.NAMESPACE_MODE_REQUESTER);
updateNotification = prefs.getBoolean("notification", true);
updateChannel = Utils.getPrefsInt(prefs, "update_channel", CheckUpdates.STABLE_CHANNEL);
bootFormat = prefs.getString("boot_format", ".img");
snet_version = prefs.getInt("snet_version", -1);
}
public void toast(String msg, int duration) {
mHandler.post(() -> Toast.makeText(this, msg, duration).show());
}
public void toast(int resId, int duration) {
mHandler.post(() -> Toast.makeText(this, resId, duration).show());
}
public void startup() {
if (started)
return;
started = true;
boolean hasNetwork = Utils.checkNetworkStatus(this);
getMagiskInfo();
// Check if we need to migrate suDB
if (getDatabasePath(SuDatabaseHelper.DB_NAME).exists() && Utils.useFDE(this)) {
if (magiskVersionCode >= 1410) {
suDB.close();
Context de = createDeviceProtectedStorageContext();
de.moveDatabaseFrom(this, SuDatabaseHelper.DB_NAME);
suDB = new SuDatabaseHelper(de);
}
}
new LoadLocale(this).exec();
// Root actions
if (Shell.rootAccess()) {
if (hasNetwork && !Utils.itemExist(shell, BUSYBOXPATH + "/busybox")) {
try {
// Force synchronous, make sure we have busybox to use
new DownloadBusybox(this).exec().get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
try (InputStream in = getAssets().open(Utils.UTIL_FUNCTIONS)) {
shell.loadInputStream(in);
} catch (IOException e) {
e.printStackTrace();
}
shell.su_raw(
"export PATH=" + BUSYBOXPATH + ":$PATH",
"mount_partitions",
"BOOTIMAGE=",
"find_boot_image",
"migrate_boot_backup"
);
List<String> res = shell.su("echo \"$BOOTIMAGE\"");
if (Utils.isValidShellResponse(res)) {
bootBlock = res.get(0);
} else {
blockList = shell.su("find /dev/block -type b | grep -vE 'dm|ram|loop'");
}
}
// Write back default values
prefs.edit()
.putBoolean("dark_theme", isDarkTheme)
.putBoolean("magiskhide", magiskHide)
.putBoolean("notification", updateNotification)
.putBoolean("hosts", Utils.itemExist(shell, MAGISK_HOST_FILE))
.putBoolean("disable", Utils.itemExist(shell, MAGISK_DISABLE_FILE))
.putBoolean("su_reauth", suReauth)
.putString("su_request_timeout", String.valueOf(suRequestTimeout))
.putString("su_auto_response", String.valueOf(suResponseType))
.putString("su_notification", String.valueOf(suNotificationType))
.putString("su_access", String.valueOf(suAccessState))
.putString("multiuser_mode", String.valueOf(multiuserMode))
.putString("mnt_ns", String.valueOf(suNamespaceMode))
.putString("update_channel", String.valueOf(updateChannel))
.putString("locale", localeConfig)
.putString("boot_format", bootFormat)
.apply();
// Create notification channel on Android O
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL,
getString(R.string.magisk_updates), NotificationManager.IMPORTANCE_DEFAULT);
getSystemService(NotificationManager.class).createNotificationChannel(channel);
}
LoadModules loadModuleTask = new LoadModules(this);
// Start update check job
if (hasNetwork) {
ComponentName service = new ComponentName(this, UpdateCheckService.class);
JobInfo jobInfo = new JobInfo.Builder(UPDATE_SERVICE_ID, service)
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setPersisted(true)
.setPeriodic(8 * 60 * 60 * 1000)
.build();
((JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE)).schedule(jobInfo);
loadModuleTask.setCallBack(() -> new UpdateRepos(this, false).exec());
}
// Fire asynctasks
loadModuleTask.exec();
}
public void getMagiskInfo() {
List<String> ret;
Shell.getShell(this);
ret = shell.sh("su -v");
if (Utils.isValidShellResponse(ret)) {
suVersion = ret.get(0);
isSuClient = suVersion.toUpperCase().contains("MAGISK");
}
ret = shell.sh("magisk -v");
if (!Utils.isValidShellResponse(ret)) {
ret = shell.sh("getprop magisk.version");
if (Utils.isValidShellResponse(ret)) {
try {
magiskVersionString = ret.get(0);
magiskVersionCode = (int) Double.parseDouble(ret.get(0)) * 10;
} catch (NumberFormatException ignored) {}
}
} else {
magiskVersionString = ret.get(0).split(":")[0];
ret = shell.sh("magisk -V");
try {
magiskVersionCode = Integer.parseInt(ret.get(0));
} catch (NumberFormatException ignored) {}
}
ret = shell.sh("getprop " + DISABLE_INDICATION_PROP);
try {
disabled = Utils.isValidShellResponse(ret) && Integer.parseInt(ret.get(0)) != 0;
} catch (NumberFormatException e) {
disabled = false;
}
if (magiskVersionCode > 1435) {
ret = shell.su("resetprop -p " + MAGISKHIDE_PROP);
} else {
ret = shell.sh("getprop " + MAGISKHIDE_PROP);
}
try {
magiskHide = !Utils.isValidShellResponse(ret) || Integer.parseInt(ret.get(0)) != 0;
} catch (NumberFormatException e) {
magiskHide = true;
}
}
}

View File

@@ -1,78 +1,57 @@
package com.topjohnwu.magisk;
import android.Manifest;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.design.widget.NavigationView;
import android.support.v4.app.ActivityCompat;
import android.support.v4.view.GravityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import com.topjohnwu.magisk.utils.CallbackHandler;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import butterknife.BindView;
import butterknife.ButterKnife;
public class MainActivity extends AppCompatActivity
implements NavigationView.OnNavigationItemSelectedListener, CallbackHandler.EventListener {
public static AlertDialog.Builder alertBuilder = null;
private static final String SELECTED_ITEM_ID = "SELECTED_ITEM_ID";
public class MainActivity extends Activity
implements NavigationView.OnNavigationItemSelectedListener, Topic.Subscriber {
private final Handler mDrawerHandler = new Handler();
private SharedPreferences prefs;
private int mDrawerItem;
@BindView(R.id.toolbar) Toolbar toolbar;
@BindView(R.id.drawer_layout) DrawerLayout drawer;
@BindView(R.id.nav_view) public NavigationView navigationView;
@IdRes
private int mSelectedId = R.id.status;
private float toolbarElevation;
@Override
protected void onCreate(final Bundle savedInstanceState) {
getMagiskManager().startup();
prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
prefs = getMagiskManager().prefs;
String theme = prefs.getString("theme", "");
Logger.dev("MainActivity: Theme is " + theme);
if (theme.equals("Dark")) {
setTheme(R.style.AppTheme_dh);
alertBuilder = new AlertDialog.Builder(this, R.style.AlertDialog_dh);
} else {
alertBuilder = new AlertDialog.Builder(this);
if (getMagiskManager().isDarkTheme) {
setTheme(R.style.AppTheme_Dark);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
}
setSupportActionBar(toolbar);
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close) {
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.magisk, R.string.magisk) {
@Override
public void onDrawerOpened(View drawerView) {
super.onDrawerOpened(drawerView);
@@ -85,49 +64,30 @@ public class MainActivity extends AppCompatActivity
}
};
toolbarElevation = toolbar.getElevation();
drawer.addDrawerListener(toggle);
toggle.syncState();
//noinspection ResourceType
mSelectedId = savedInstanceState == null ? mSelectedId : savedInstanceState.getInt(SELECTED_ITEM_ID);
navigationView.setCheckedItem(mSelectedId);
if (savedInstanceState == null) {
mDrawerHandler.removeCallbacksAndMessages(null);
mDrawerHandler.postDelayed(() -> navigate(mSelectedId), 250);
}
if (savedInstanceState == null)
navigate(getIntent().getStringExtra(MagiskManager.INTENT_SECTION));
navigationView.setNavigationItemSelectedListener(this);
if (StatusFragment.updateCheckDone.isTriggered) {
onTrigger(StatusFragment.updateCheckDone);
}
}
@Override
protected void onResume() {
super.onResume();
CallbackHandler.register(StatusFragment.updateCheckDone, this);
checkHideSection();
}
@Override
protected void onDestroy() {
super.onDestroy();
CallbackHandler.unRegister(StatusFragment.updateCheckDone, this);
alertBuilder = null;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(SELECTED_ITEM_ID, mSelectedId);
}
@Override
public void onBackPressed() {
if (drawer.isDrawerOpen(GravityCompat.START)) {
drawer.closeDrawer(GravityCompat.START);
if (drawer.isDrawerOpen(navigationView)) {
drawer.closeDrawer(navigationView);
} else if (mDrawerItem != R.id.magisk) {
navigate(R.id.magisk);
} else {
finish();
}
@@ -135,70 +95,109 @@ public class MainActivity extends AppCompatActivity
@Override
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
mSelectedId = menuItem.getItemId();
mDrawerHandler.removeCallbacksAndMessages(null);
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
drawer.closeDrawer(GravityCompat.START);
drawer.closeDrawer(navigationView);
return true;
}
@Override
public void onTrigger(CallbackHandler.Event event) {
Menu menu = navigationView.getMenu();
menu.findItem(R.id.install).setVisible(StatusFragment.remoteMagiskVersion > 0 &&
Shell.rootAccess());
public void onTopicPublished(Topic topic, Object result) {
recreate();
}
private void checkHideSection() {
Menu menu = navigationView.getMenu();
menu.findItem(R.id.magiskhide).setVisible(StatusFragment.magiskVersion > 0 &&
prefs.getBoolean("magiskhide", false) && Shell.rootAccess());
menu.findItem(R.id.modules).setVisible(StatusFragment.magiskVersion > 0);
menu.findItem(R.id.downloads).setVisible(StatusFragment.magiskVersion > 0);
@Override
public Topic[] getSubscription() {
return new Topic[] { getMagiskManager().reloadActivity };
}
public void navigate(final int itemId) {
Fragment navFragment = null;
String tag = "";
public void checkHideSection() {
Menu menu = navigationView.getMenu();
menu.findItem(R.id.magiskhide).setVisible(
Shell.rootAccess() && getMagiskManager().magiskVersionCode >= 1300
&& prefs.getBoolean("magiskhide", false));
menu.findItem(R.id.modules).setVisible(
Shell.rootAccess() && getMagiskManager().magiskVersionCode >= 0);
menu.findItem(R.id.downloads).setVisible(Utils.checkNetworkStatus(this) &&
Shell.rootAccess() && getMagiskManager().magiskVersionCode >= 0);
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
menu.findItem(R.id.superuser).setVisible(
Shell.rootAccess() && getMagiskManager().isSuClient);
}
public void navigate(String item) {
int itemId = R.id.magisk;
if (item != null) {
switch (item) {
case "magisk":
itemId = R.id.magisk;
break;
case "superuser":
itemId = R.id.superuser;
break;
case "modules":
itemId = R.id.modules;
break;
case "downloads":
itemId = R.id.downloads;
break;
case "magiskhide":
itemId = R.id.magiskhide;
break;
case "log":
itemId = R.id.log;
break;
case "settings":
itemId = R.id.settings;
break;
case "about":
itemId = R.id.app_about;
break;
}
}
navigate(itemId);
}
public void navigate(int itemId) {
int bak = mDrawerItem;
mDrawerItem = itemId;
navigationView.setCheckedItem(itemId);
switch (itemId) {
case R.id.status:
tag = "status";
navFragment = new StatusFragment();
case R.id.magisk:
displayFragment(new MagiskFragment(), "magisk", true);
break;
case R.id.install:
tag = "install";
navFragment = new InstallFragment();
case R.id.superuser:
displayFragment(new SuperuserFragment(), "superuser", true);
break;
case R.id.modules:
tag = "modules";
navFragment = new ModulesFragment();
displayFragment(new ModulesFragment(), "modules", true);
break;
case R.id.downloads:
tag = "downloads";
navFragment = new ReposFragment();
displayFragment(new ReposFragment(), "downloads", true);
break;
case R.id.magiskhide:
tag = "magiskhide";
navFragment = new MagiskHideFragment();
displayFragment(new MagiskHideFragment(), "magiskhide", true);
break;
case R.id.log:
tag = "log";
navFragment = new LogFragment();
displayFragment(new LogFragment(), "log", false);
break;
case R.id.settings:
startActivity(new Intent(this, SettingsActivity.class));
mDrawerItem = bak;
break;
case R.id.app_about:
startActivity(new Intent(this, AboutActivity.class));
return;
}
if (navFragment != null) {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
try {
transaction.replace(R.id.content_frame, navFragment, tag).commit();
} catch (IllegalStateException ignored) {}
mDrawerItem = bak;
break;
}
}
}
private void displayFragment(@NonNull Fragment navFragment, String tag, boolean setElevation) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
supportInvalidateOptionsMenu();
transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
transaction.replace(R.id.content_frame, navFragment, tag).commitNow();
if (setElevation) toolbar.setElevation(toolbarElevation);
else toolbar.setElevation(0);
}
}

View File

@@ -1,9 +1,8 @@
package com.topjohnwu.magisk;
import android.Manifest;
import android.app.Activity;
import android.app.Fragment;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
@@ -13,49 +12,49 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.github.clans.fab.FloatingActionButton;
import com.topjohnwu.magisk.adapters.ModulesAdapter;
import com.topjohnwu.magisk.module.Module;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.CallbackHandler;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.ModuleHelper;
import com.topjohnwu.magisk.asyncs.LoadModules;
import com.topjohnwu.magisk.components.Fragment;
import com.topjohnwu.magisk.container.Module;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
public class ModulesFragment extends Fragment implements CallbackHandler.EventListener {
public static final CallbackHandler.Event moduleLoadDone = new CallbackHandler.Event();
public class ModulesFragment extends Fragment implements Topic.Subscriber {
private static final int FETCH_ZIP_CODE = 2;
private Unbinder unbinder;
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.empty_rv) TextView emptyTv;
@BindView(R.id.fab) FloatingActionButton fabio;
private List<Module> listModules = new ArrayList<>();
private View mView;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mView = inflater.inflate(R.layout.modules_fragment, container, false);
ButterKnife.bind(this, mView);
fabio.setOnClickListener(v -> {
@BindView(R.id.empty_rv) TextView emptyRv;
@OnClick(R.id.fab)
public void selectFile() {
Utils.runWithPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("application/zip");
startActivityForResult(intent, FETCH_ZIP_CODE);
});
}
private List<Module> listModules = new ArrayList<>();
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_modules, container, false);
unbinder = ButterKnife.bind(this, view);
mSwipeRefreshLayout.setOnRefreshListener(() -> {
recyclerView.setVisibility(View.GONE);
new Async.LoadModules().exec();
new LoadModules(getActivity()).exec();
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@@ -70,50 +69,45 @@ public class ModulesFragment extends Fragment implements CallbackHandler.EventLi
}
});
if (moduleLoadDone.isTriggered) {
updateUI();
}
getActivity().setTitle(R.string.modules);
return mView;
return view;
}
@Override
public void onTrigger(CallbackHandler.Event event) {
Logger.dev("ModulesFragment: UI refresh triggered");
public void onTopicPublished(Topic topic, Object result) {
updateUI();
}
@Override
public Topic[] getSubscription() {
return new Topic[] { getApplication().moduleLoadDone };
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == FETCH_ZIP_CODE && resultCode == Activity.RESULT_OK && data != null) {
// Get the URI of the selected file
final Uri uri = data.getData();
new Async.FlashZIP(getActivity(), uri).exec();
Intent intent = new Intent(getActivity(), FlashActivity.class);
intent.setData(data.getData()).putExtra(FlashActivity.SET_ACTION, FlashActivity.FLASH_ZIP);
startActivity(intent);
}
}
@Override
public void onResume() {
super.onResume();
mView = this.getView();
CallbackHandler.register(moduleLoadDone, this);
getActivity().setTitle(R.string.modules);
}
@Override
public void onDestroy() {
super.onDestroy();
CallbackHandler.unRegister(moduleLoadDone, this);
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
private void updateUI() {
ModuleHelper.getModuleList(listModules);
listModules.clear();
listModules.addAll(getApplication().moduleMap.values());
if (listModules.size() == 0) {
emptyTv.setVisibility(View.VISIBLE);
emptyRv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyTv.setVisibility(View.GONE);
emptyRv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
recyclerView.setAdapter(new ModulesAdapter(listModules));
}

View File

@@ -1,9 +1,7 @@
package com.topjohnwu.magisk;
import android.app.Fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.view.MenuItemCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
@@ -15,65 +13,78 @@ import android.widget.SearchView;
import android.widget.TextView;
import com.topjohnwu.magisk.adapters.ReposAdapter;
import com.topjohnwu.magisk.adapters.SimpleSectionedRecyclerViewAdapter;
import com.topjohnwu.magisk.module.Repo;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.CallbackHandler;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.ModuleHelper;
import java.util.ArrayList;
import java.util.List;
import com.topjohnwu.magisk.asyncs.UpdateRepos;
import com.topjohnwu.magisk.components.Fragment;
import com.topjohnwu.magisk.utils.Topic;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class ReposFragment extends Fragment implements CallbackHandler.EventListener {
public static final CallbackHandler.Event repoLoadDone = new CallbackHandler.Event();
public class ReposFragment extends Fragment implements Topic.Subscriber {
private Unbinder unbinder;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.empty_rv) TextView emptyTv;
@BindView(R.id.empty_rv) TextView emptyRv;
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
private List<Repo> mUpdateRepos = new ArrayList<>();
private List<Repo> mInstalledRepos = new ArrayList<>();
private List<Repo> mOthersRepos = new ArrayList<>();
private List<Repo> fUpdateRepos = new ArrayList<>();
private List<Repo> fInstalledRepos = new ArrayList<>();
private List<Repo> fOthersRepos = new ArrayList<>();
private SimpleSectionedRecyclerViewAdapter mSectionedAdapter;
private SearchView.OnQueryTextListener searchListener;
@Nullable
public static ReposAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.repos_fragment, container, false);
ButterKnife.bind(this, view);
mSectionedAdapter = new
SimpleSectionedRecyclerViewAdapter(getActivity(), R.layout.section,
R.id.section_text, new ReposAdapter(fUpdateRepos, fInstalledRepos, fOthersRepos));
recyclerView.setAdapter(mSectionedAdapter);
View view = inflater.inflate(R.layout.fragment_repos, container, false);
unbinder = ButterKnife.bind(this, view);
mSwipeRefreshLayout.setRefreshing(true);
mSwipeRefreshLayout.setOnRefreshListener(() -> {
recyclerView.setVisibility(View.GONE);
new Async.LoadRepos(getActivity()).exec();
recyclerView.setVisibility(View.VISIBLE);
emptyRv.setVisibility(View.GONE);
new UpdateRepos(getActivity(), true).exec();
});
if (repoLoadDone.isTriggered) {
reloadRepos();
updateUI();
}
getActivity().setTitle(R.string.downloads);
searchListener = new SearchView.OnQueryTextListener() {
return view;
}
@Override
public void onResume() {
adapter = new ReposAdapter(getApplication().repoDB, getApplication().moduleMap);
recyclerView.setAdapter(adapter);
super.onResume();
}
@Override
public void onPause() {
super.onPause();
adapter = null;
}
@Override
public void onTopicPublished(Topic topic, Object result) {
mSwipeRefreshLayout.setRefreshing(false);
recyclerView.setVisibility(adapter.getItemCount() == 0 ? View.GONE : View.VISIBLE);
emptyRv.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE);
}
@Override
public Topic[] getSubscription() {
return new Topic[] { getApplication().repoLoadDone };
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_repo, menu);
SearchView search = (SearchView) menu.findItem(R.id.repo_search).getActionView();
search.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
@@ -81,113 +92,15 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
@Override
public boolean onQueryTextChange(String newText) {
new FilterApps().exec(newText);
adapter.filter(newText);
return false;
}
};
return view;
});
}
@Override
public void onTrigger(CallbackHandler.Event event) {
Logger.dev("ReposFragment: UI refresh triggered");
reloadRepos();
updateUI();
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_repo, menu);
SearchView search = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.repo_search));
search.setOnQueryTextListener(searchListener);
}
@Override
public void onResume() {
super.onResume();
setHasOptionsMenu(true);
CallbackHandler.register(repoLoadDone, this);
getActivity().setTitle(R.string.downloads);
}
@Override
public void onDestroy() {
super.onDestroy();
CallbackHandler.unRegister(repoLoadDone, this);
}
private void reloadRepos() {
ModuleHelper.getRepoLists(mUpdateRepos, mInstalledRepos, mOthersRepos);
fUpdateRepos.clear();
fInstalledRepos.clear();
fOthersRepos.clear();
fUpdateRepos.addAll(mUpdateRepos);
fInstalledRepos.addAll(mInstalledRepos);
fOthersRepos.addAll(mOthersRepos);
}
private void updateUI() {
if (fUpdateRepos.size() + fInstalledRepos.size() + fOthersRepos.size() == 0) {
emptyTv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
List<SimpleSectionedRecyclerViewAdapter.Section> sections = new ArrayList<>();
if (!fUpdateRepos.isEmpty()) {
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(0, getString(R.string.update_available)));
}
if (!fInstalledRepos.isEmpty()) {
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(fUpdateRepos.size(), getString(R.string.installed)));
}
if (!fOthersRepos.isEmpty()) {
sections.add(new SimpleSectionedRecyclerViewAdapter.Section(fUpdateRepos.size() + fInstalledRepos.size(), getString(R.string.not_installed)));
}
SimpleSectionedRecyclerViewAdapter.Section[] array = sections.toArray(new SimpleSectionedRecyclerViewAdapter.Section[sections.size()]);
mSectionedAdapter.setSections(array);
emptyTv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
mSwipeRefreshLayout.setRefreshing(false);
}
private class FilterApps extends Async.NormalTask<String, Void, Void> {
@Override
protected Void doInBackground(String... strings) {
String newText = strings[0];
fUpdateRepos.clear();
fInstalledRepos.clear();
fOthersRepos.clear();
for (Repo repo: mUpdateRepos) {
if (repo.getName().toLowerCase().contains(newText.toLowerCase())
|| repo.getAuthor().toLowerCase().contains(newText.toLowerCase())
|| repo.getDescription().toLowerCase().contains(newText.toLowerCase())
) {
fUpdateRepos.add(repo);
}
}
for (Repo repo: mInstalledRepos) {
if (repo.getName().toLowerCase().contains(newText.toLowerCase())
|| repo.getAuthor().toLowerCase().contains(newText.toLowerCase())
|| repo.getDescription().toLowerCase().contains(newText.toLowerCase())
) {
fInstalledRepos.add(repo);
}
}
for (Repo repo: mOthersRepos) {
if (repo.getName().toLowerCase().contains(newText.toLowerCase())
|| repo.getAuthor().toLowerCase().contains(newText.toLowerCase())
|| repo.getDescription().toLowerCase().contains(newText.toLowerCase())
) {
fOthersRepos.add(repo);
}
}
return null;
}
@Override
protected void onPostExecute(Void v) {
updateUI();
}
}
}

View File

@@ -1,40 +1,45 @@
package com.topjohnwu.magisk;
import android.content.Intent;
import android.Manifest;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.SwitchPreference;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.WindowManager;
import android.widget.Toast;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.asyncs.CheckUpdates;
import com.topjohnwu.magisk.asyncs.HideManager;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.database.SuDatabaseHelper;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import java.util.Locale;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SettingsActivity extends AppCompatActivity {
public class SettingsActivity extends Activity implements Topic.Subscriber {
@BindView(R.id.toolbar) Toolbar toolbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String theme = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("theme", "");
Logger.dev("AboutActivity: Theme is " + theme);
if (theme.equals("Dark")) {
setTheme(R.style.AppTheme_dh);
if (getMagiskManager().isDarkTheme) {
setTheme(R.style.AppTheme_Transparent_Dark);
}
setContentView(R.layout.activity_container);
setContentView(R.layout.activity_settings);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
@@ -55,49 +60,110 @@ public class SettingsActivity extends AppCompatActivity {
}
public void setFloating() {
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
if (isTablet) {
WindowManager.LayoutParams params = getWindow().getAttributes();
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
params.alpha = 1.0f;
params.dimAmount = 0.6f;
params.flags |= 2;
getWindow().setAttributes(params);
setFinishOnTouchOutside(true);
}
@Override
public void onTopicPublished(Topic topic, Object result) {
recreate();
}
@Override
public Topic[] getSubscription() {
return new Topic[] { getMagiskManager().reloadActivity };
}
public static class SettingsFragment extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
implements SharedPreferences.OnSharedPreferenceChangeListener,
Topic.Subscriber {
private ListPreference themePreference;
private SharedPreferences prefs;
private PreferenceScreen prefScreen;
private ListPreference updateChannel, suAccess, autoRes, suNotification,
requestTimeout, multiuserMode, namespaceMode;
private MagiskManager mm;
private PreferenceCategory generalCatagory;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.app_settings);
PreferenceManager.setDefaultValues(getActivity(), R.xml.app_settings, false);
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
prefScreen = getPreferenceScreen();
mm = Utils.getMagiskManager(getActivity());
themePreference = (ListPreference) findPreference("theme");
CheckBoxPreference busyboxPreference = (CheckBoxPreference) findPreference("busybox");
CheckBoxPreference magiskhidePreference = (CheckBoxPreference) findPreference("magiskhide");
CheckBoxPreference hostsPreference = (CheckBoxPreference) findPreference("hosts");
generalCatagory = (PreferenceCategory) findPreference("general");
PreferenceCategory magiskCategory = (PreferenceCategory) findPreference("magisk");
PreferenceCategory suCategory = (PreferenceCategory) findPreference("superuser");
themePreference.setSummary(themePreference.getValue());
updateChannel = (ListPreference) findPreference("update_channel");
suAccess = (ListPreference) findPreference("su_access");
autoRes = (ListPreference) findPreference("su_auto_response");
requestTimeout = (ListPreference) findPreference("su_request_timeout");
suNotification = (ListPreference) findPreference("su_notification");
multiuserMode = (ListPreference) findPreference("multiuser_mode");
namespaceMode = (ListPreference) findPreference("mnt_ns");
SwitchPreference reauth = (SwitchPreference) findPreference("su_reauth");
Preference hideManager = findPreference("hide");
if (StatusFragment.magiskVersion < 9) {
hostsPreference.setEnabled(false);
busyboxPreference.setEnabled(false);
} else if (StatusFragment.magiskVersion < 8) {
magiskhidePreference.setEnabled(false);
setSummary();
// Disable dangerous settings in user mode if selected owner manage
if (getActivity().getApplicationInfo().uid > 99999) {
suCategory.removePreference(multiuserMode);
generalCatagory.removePreference(hideManager);
}
// Remove re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
suCategory.removePreference(reauth);
}
findPreference("clear").setOnPreferenceClickListener((pref) -> {
Utils.clearRepoCache(getActivity());
return true;
});
hideManager.setOnPreferenceClickListener((pref) -> {
Utils.runWithPermission(getActivity(),
Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> new HideManager(getActivity()).exec());
return true;
});
if (!Shell.rootAccess()) {
prefScreen.removePreference(magiskCategory);
prefScreen.removePreference(suCategory);
generalCatagory.removePreference(hideManager);
} else {
busyboxPreference.setEnabled(true);
magiskhidePreference.setEnabled(true);
hostsPreference.setEnabled(true);
if (!mm.isSuClient) {
prefScreen.removePreference(suCategory);
}
if (mm.magiskVersionCode < 1300) {
prefScreen.removePreference(magiskCategory);
}
}
}
private void setLocalePreference(ListPreference lp) {
boolean isNew = lp == null;
if (isNew) {
lp = new ListPreference(getActivity());
}
CharSequence[] entries = new CharSequence[mm.locales.size() + 1];
CharSequence[] entryValues = new CharSequence[mm.locales.size() + 1];
entries[0] = getString(R.string.system_default);
entryValues[0] = "";
int i = 1;
for (Locale locale : mm.locales) {
entries[i] = locale.getDisplayName(locale);
entryValues[i++] = locale.toLanguageTag();
}
lp.setEntries(entries);
lp.setEntryValues(entryValues);
lp.setTitle(R.string.language);
lp.setKey("locale");
lp.setSummary(MagiskManager.locale.getDisplayName(MagiskManager.locale));
if (isNew) {
generalCatagory.addPreference(lp);
}
}
@@ -105,105 +171,107 @@ public class SettingsActivity extends AppCompatActivity {
public void onResume() {
super.onResume();
prefs.registerOnSharedPreferenceChangeListener(this);
subscribeTopics();
}
@Override
public void onDestroy() {
super.onDestroy();
public void onPause() {
prefs.unregisterOnSharedPreferenceChangeListener(this);
unsubscribeTopics();
super.onPause();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
Logger.dev("Settings: Prefs change " + key);
boolean checked;
boolean enabled;
switch (key) {
case "theme":
String theme = prefs.getString(key, "");
themePreference.setSummary(theme);
if (theme.equals("Dark")) {
getActivity().getApplication().setTheme(R.style.AppTheme_dh);
} else {
getActivity().getApplication().setTheme(R.style.AppTheme);
case "dark_theme":
enabled = prefs.getBoolean("dark_theme", false);
if (mm.isDarkTheme != enabled) {
mm.reloadActivity.publish(false);
}
Intent intent = new Intent(getActivity(), MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
break;
case "disable":
enabled = prefs.getBoolean("disable", false);
if (enabled) {
Utils.createFile(getShell(), MagiskManager.MAGISK_DISABLE_FILE);
} else {
Utils.removeItem(getShell(), MagiskManager.MAGISK_DISABLE_FILE);
}
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
break;
case "magiskhide":
checked = prefs.getBoolean("magiskhide", false);
if (checked) {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Utils.createFile("/magisk/.core/magiskhide/enable");
return null;
}
}.exec();
enabled = prefs.getBoolean("magiskhide", false);
if (enabled) {
Utils.enableMagiskHide(getShell());
} else {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Utils.removeItem("/magisk/.core/magiskhide/enable");
return null;
}
}.exec();
Utils.disableMagiskHide(getShell());
}
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
break;
case "busybox":
checked = prefs.getBoolean("busybox", false);
if (checked) {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Utils.createFile("/magisk/.core/busybox/enable");
return null;
}
}.exec();
} else {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Utils.removeItem("/magisk/.core/busybox/enable");
return null;
}
}.exec();
}
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
break;
case "hosts":
checked = prefs.getBoolean("hosts", false);
if (checked) {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
Shell.su("cp -af /system/etc/hosts /magisk/.core/hosts");
return null;
}
}.exec();
enabled = prefs.getBoolean("hosts", false);
if (enabled) {
getShell().su_raw(
"cp -af /system/etc/hosts " + MagiskManager.MAGISK_HOST_FILE,
"mount -o bind " + MagiskManager.MAGISK_HOST_FILE + " /system/etc/hosts");
} else {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
Shell.su("umount -l /system/etc/hosts", "rm -f /magisk/.core/hosts");
return null;
}
}.exec();
getShell().su_raw(
"umount -l /system/etc/hosts",
"rm -f " + MagiskManager.MAGISK_HOST_FILE);
}
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
break;
case "developer_logging":
Logger.devLog = prefs.getBoolean("developer_logging", false);
case "su_access":
mm.suDB.setSettings(SuDatabaseHelper.ROOT_ACCESS, Utils.getPrefsInt(prefs, "su_access"));
break;
case "shell_logging":
Logger.logShell = prefs.getBoolean("shell_logging", false);
case "multiuser_mode":
mm.suDB.setSettings(SuDatabaseHelper.MULTIUSER_MODE, Utils.getPrefsInt(prefs, "multiuser_mode"));
break;
case "mnt_ns":
mm.suDB.setSettings(SuDatabaseHelper.MNT_NS, Utils.getPrefsInt(prefs, "mnt_ns"));
break;
case "locale":
mm.setLocale();
mm.reloadActivity.publish(false);
break;
case "update_channel":
mm.updateChannel = Utils.getPrefsInt(prefs, "update_channel");
new CheckUpdates(mm, true).exec();
break;
}
mm.loadConfig();
setSummary();
}
private Shell getShell() {
return Shell.getShell(getActivity());
}
private void setSummary() {
updateChannel.setSummary(getResources()
.getStringArray(R.array.update_channel)[mm.updateChannel]);
suAccess.setSummary(getResources()
.getStringArray(R.array.su_access)[mm.suAccessState]);
autoRes.setSummary(getResources()
.getStringArray(R.array.auto_response)[mm.suResponseType]);
suNotification.setSummary(getResources()
.getStringArray(R.array.su_notification)[mm.suNotificationType]);
requestTimeout.setSummary(
getString(R.string.request_timeout_summary, prefs.getString("su_request_timeout", "10")));
multiuserMode.setSummary(getResources()
.getStringArray(R.array.multiuser_summary)[mm.multiuserMode]);
namespaceMode.setSummary(getResources()
.getStringArray(R.array.namespace_summary)[mm.suNamespaceMode]);
}
@Override
public void onTopicPublished(Topic topic, Object result) {
setLocalePreference((ListPreference) findPreference("locale"));
}
@Override
public Topic[] getSubscription() {
return new Topic[] { mm.localeDone };
}
}

View File

@@ -1,50 +1,23 @@
package com.topjohnwu.magisk;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatActivity;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.components.Activity;
public class SplashActivity extends AppCompatActivity {
public class SplashActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplication());
if (prefs.getString("theme", "").equals("Dark")) {
setTheme(R.style.AppTheme_dh);
getMagiskManager().startup();
Intent intent = new Intent(this, MainActivity.class);
String section = getIntent().getStringExtra(MagiskManager.INTENT_SECTION);
if (section != null) {
intent.putExtra(MagiskManager.INTENT_SECTION, section);
}
Logger.devLog = prefs.getBoolean("developer_logging", false);
Logger.logShell = prefs.getBoolean("shell_logging", false);
// Initialize prefs
prefs.edit()
.putBoolean("magiskhide", Utils.itemExist(false, "/magisk/.core/magiskhide/enable"))
.putBoolean("busybox", Utils.commandExists("busybox"))
.putBoolean("hosts", Utils.itemExist(false, "/magisk/.core/hosts"))
.apply();
// Start all async tasks
new Async.GetBootBlocks().exec();
new Async.CheckUpdates().exec();
Async.checkSafetyNet(getApplicationContext());
new Async.LoadModules() {
@Override
protected void onPostExecute(Void v) {
super.onPostExecute(v);
new Async.LoadRepos(getApplicationContext()).exec();
}
}.exec();
// Start main activity
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
finish();
}

View File

@@ -1,246 +0,0 @@
package com.topjohnwu.magisk;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.CallbackHandler;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Shell;
import java.util.List;
import butterknife.BindColor;
import butterknife.BindView;
import butterknife.ButterKnife;
public class StatusFragment extends Fragment implements CallbackHandler.EventListener {
public static double magiskVersion, remoteMagiskVersion = -1;
public static String magiskVersionString, magiskLink, magiskChangelog;
public static int SNCheckResult = -1;
public static final CallbackHandler.Event updateCheckDone = new CallbackHandler.Event();
public static final CallbackHandler.Event safetyNetDone = new CallbackHandler.Event();
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
@BindView(R.id.magisk_status_container) View magiskStatusContainer;
@BindView(R.id.magisk_status_icon) ImageView magiskStatusIcon;
@BindView(R.id.magisk_version) TextView magiskVersionText;
@BindView(R.id.magisk_update_status) TextView magiskUpdateText;
@BindView(R.id.magisk_check_updates_progress) ProgressBar magiskCheckUpdatesProgress;
@BindView(R.id.root_status_container) View rootStatusContainer;
@BindView(R.id.root_status_icon) ImageView rootStatusIcon;
@BindView(R.id.root_status) TextView rootStatusText;
@BindView(R.id.root_info) TextView rootInfoText;
@BindView(R.id.safetyNet_container) View safetyNetContainer;
@BindView(R.id.safetyNet_icon) ImageView safetyNetIcon;
@BindView(R.id.safetyNet_status) TextView safetyNetStatusText;
@BindView(R.id.safetyNet_check_progress) ProgressBar safetyNetProgress;
@BindColor(R.color.red500) int colorBad;
@BindColor(R.color.green500) int colorOK;
@BindColor(R.color.yellow500) int colorWarn;
@BindColor(R.color.grey500) int colorNeutral;
@BindColor(R.color.blue500) int colorInfo;
@BindColor(android.R.color.transparent) int trans;
static {
checkMagiskInfo();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.status_fragment, container, false);
ButterKnife.bind(this, v);
mSwipeRefreshLayout.setOnRefreshListener(() -> {
magiskStatusContainer.setBackgroundColor(trans);
magiskStatusIcon.setImageResource(0);
magiskUpdateText.setText(R.string.checking_for_updates);
magiskCheckUpdatesProgress.setVisibility(View.VISIBLE);
safetyNetProgress.setVisibility(View.VISIBLE);
safetyNetContainer.setBackgroundColor(trans);
safetyNetIcon.setImageResource(0);
safetyNetStatusText.setText(R.string.checking_safetyNet_status);
updateUI();
new Async.CheckUpdates().exec();
Async.checkSafetyNet(getActivity());
});
updateUI();
if (updateCheckDone.isTriggered) {
updateCheckUI();
}
if (safetyNetDone.isTriggered) {
updateSafetyNetUI();
}
if (magiskVersion < 0 && Shell.rootAccess()) {
MainActivity.alertBuilder
.setTitle(R.string.no_magisk_title)
.setMessage(R.string.no_magisk_msg)
.setCancelable(true)
.setPositiveButton(R.string.goto_install, (dialogInterface, i) -> {
((MainActivity) getActivity()).navigationView.setCheckedItem(R.id.install);
FragmentTransaction transaction = getFragmentManager().beginTransaction();
transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
try {
transaction.replace(R.id.content_frame, new InstallFragment(), "install").commit();
} catch (IllegalStateException ignored) {}
})
.setNegativeButton(R.string.no_thanks, null)
.show();
}
return v;
}
@Override
public void onTrigger(CallbackHandler.Event event) {
if (event == updateCheckDone) {
Logger.dev("StatusFragment: Update Check UI refresh triggered");
updateCheckUI();
} else if (event == safetyNetDone) {
Logger.dev("StatusFragment: SafetyNet UI refresh triggered");
updateSafetyNetUI();
}
}
@Override
public void onResume() {
super.onResume();
CallbackHandler.register(updateCheckDone, this);
CallbackHandler.register(safetyNetDone, this);
getActivity().setTitle(R.string.status);
}
@Override
public void onDestroy() {
super.onDestroy();
CallbackHandler.unRegister(updateCheckDone, this);
CallbackHandler.unRegister(safetyNetDone, this);
}
private static void checkMagiskInfo() {
List<String> ret = Shell.sh("getprop magisk.version");
if (ret.get(0).length() == 0) {
magiskVersion = -1;
} else {
try {
magiskVersionString = ret.get(0);
magiskVersion = Double.parseDouble(ret.get(0));
} catch (NumberFormatException e) {
// Custom version don't need to receive updates
magiskVersion = Double.POSITIVE_INFINITY;
}
}
}
private void updateUI() {
int image, color;
checkMagiskInfo();
if (magiskVersion < 0) {
magiskVersionText.setText(R.string.magisk_version_error);
} else {
magiskVersionText.setText(getString(R.string.magisk_version, magiskVersionString));
}
if (Shell.rootStatus == 1) {
color = colorOK;
image = R.drawable.ic_check_circle;
rootStatusText.setText(R.string.proper_root);
rootInfoText.setText(Shell.sh("su -v").get(0));
} else {
rootInfoText.setText(R.string.root_info_warning);
if (Shell.rootStatus == 0) {
color = colorBad;
image = R.drawable.ic_cancel;
rootStatusText.setText(R.string.not_rooted);
} else {
color = colorNeutral;
image = R.drawable.ic_help;
rootStatusText.setText(R.string.root_error);
}
}
rootStatusContainer.setBackgroundColor(color);
rootStatusText.setTextColor(color);
rootInfoText.setTextColor(color);
rootStatusIcon.setImageResource(image);
}
private void updateCheckUI() {
int image, color;
if (remoteMagiskVersion < 0) {
color = colorNeutral;
image = R.drawable.ic_help;
magiskUpdateText.setText(R.string.cannot_check_updates);
} else if (remoteMagiskVersion > magiskVersion) {
color = colorInfo;
image = R.drawable.ic_update;
magiskUpdateText.setText(getString(R.string.magisk_update_available, remoteMagiskVersion));
} else {
color = colorOK;
image = R.drawable.ic_check_circle;
magiskUpdateText.setText(getString(R.string.up_to_date, getString(R.string.magisk)));
}
if (magiskVersion < 0) {
color = colorBad;
image = R.drawable.ic_cancel;
}
magiskStatusContainer.setBackgroundColor(color);
magiskVersionText.setTextColor(color);
magiskUpdateText.setTextColor(color);
magiskStatusIcon.setImageResource(image);
magiskCheckUpdatesProgress.setVisibility(View.GONE);
mSwipeRefreshLayout.setRefreshing(false);
}
private void updateSafetyNetUI() {
int image, color;
safetyNetProgress.setVisibility(View.GONE);
switch (SNCheckResult) {
case -1:
color = colorNeutral;
image = R.drawable.ic_help;
safetyNetStatusText.setText(R.string.safetyNet_error);
break;
case 0:
color = colorBad;
image = R.drawable.ic_cancel;
safetyNetStatusText.setText(R.string.safetyNet_fail);
break;
case 1:
default:
color = colorOK;
image = R.drawable.ic_check_circle;
safetyNetStatusText.setText(R.string.safetyNet_pass);
break;
}
safetyNetContainer.setBackgroundColor(color);
safetyNetStatusText.setTextColor(color);
safetyNetIcon.setImageResource(image);
}
}

View File

@@ -0,0 +1,89 @@
package com.topjohnwu.magisk;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.topjohnwu.magisk.adapters.SuLogAdapter;
import com.topjohnwu.magisk.components.Fragment;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class SuLogFragment extends Fragment {
@BindView(R.id.empty_rv) TextView emptyRv;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
private Unbinder unbinder;
private MagiskManager mm;
private SuLogAdapter adapter;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.menu_log, menu);
menu.findItem(R.id.menu_save).setVisible(false);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View v = inflater.inflate(R.layout.fragment_su_log, container, false);
unbinder = ButterKnife.bind(this, v);
mm = getApplication();
adapter = new SuLogAdapter(mm.suDB);
recyclerView.setAdapter(adapter);
updateList();
return v;
}
private void updateList() {
adapter.notifyDBChanged();
if (adapter.getSectionCount() == 0) {
emptyRv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyRv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_refresh:
updateList();
return true;
case R.id.menu_clear:
mm.suDB.clearLogs();
updateList();
return true;
default:
return true;
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}

View File

@@ -0,0 +1,63 @@
package com.topjohnwu.magisk;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.topjohnwu.magisk.adapters.PolicyAdapter;
import com.topjohnwu.magisk.components.Fragment;
import com.topjohnwu.magisk.container.Policy;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
public class SuperuserFragment extends Fragment {
private Unbinder unbinder;
@BindView(R.id.recyclerView) RecyclerView recyclerView;
@BindView(R.id.empty_rv) TextView emptyRv;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_superuser, container, false);
unbinder = ButterKnife.bind(this, view);
PackageManager pm = getActivity().getPackageManager();
MagiskManager mm = getApplication();
List<Policy> policyList = mm.suDB.getPolicyList(pm);
if (policyList.size() == 0) {
emptyRv.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
recyclerView.setAdapter(new PolicyAdapter(policyList, mm.suDB, pm));
emptyRv.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
return view;
}
@Override
public void onStart() {
super.onStart();
getActivity().setTitle(getString(R.string.superuser));
}
@Override
public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}

View File

@@ -3,20 +3,27 @@ package com.topjohnwu.magisk.adapters;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.asyncs.ParallelTask;
import com.topjohnwu.magisk.components.SnackbarMaker;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import butterknife.BindView;
@@ -24,32 +31,37 @@ import butterknife.ButterKnife;
public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.ViewHolder> {
private List<ApplicationInfo> mList;
private List<String> mHideList;
private Context context;
private PackageManager packageManager;
// Don't show in list...
private static final String[] blackList = {
public static final List<String> BLACKLIST = Arrays.asList(
"android",
"com.topjohnwu.magisk",
"com.google.android.gms"
};
);
public ApplicationAdapter(List<ApplicationInfo> list, List<String> hideList) {
mList = list;
mHideList = hideList;
List<String> bl = Arrays.asList(blackList);
for (int i = 0; i < mList.size(); ++i)
if (bl.contains(mList.get(i).packageName))
mList.remove(i);
private static final List<String> SNLIST = Arrays.asList(
"com.google.android.apps.walletnfcrel",
"com.nianticlabs.pokemongo"
);
private List<ApplicationInfo> mOriginalList, mList;
private List<String> mHideList;
private PackageManager pm;
private ApplicationFilter filter;
private Topic magiskHideDone;
private Shell shell;
public ApplicationAdapter(Context context) {
mOriginalList = mList = Collections.emptyList();
mHideList = Collections.emptyList();
filter = new ApplicationFilter();
pm = context.getPackageManager();
magiskHideDone = Utils.getMagiskManager(context).magiskHideDone;
shell = Shell.getShell(context);
new LoadApps().exec();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false);
context = parent.getContext();
packageManager = context.getPackageManager();
ButterKnife.bind(this, mView);
return new ViewHolder(mView);
}
@@ -57,29 +69,34 @@ public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.
public void onBindViewHolder(final ViewHolder holder, int position) {
ApplicationInfo info = mList.get(position);
holder.appIcon.setImageDrawable(info.loadIcon(packageManager));
holder.appName.setText(info.loadLabel(packageManager));
holder.appIcon.setImageDrawable(info.loadIcon(pm));
holder.appName.setText(info.loadLabel(pm));
holder.appPackage.setText(info.packageName);
holder.checkBox.setChecked(false);
for (String hidePackage : mHideList) {
if (info.packageName.contains(hidePackage)) {
holder.checkBox.setChecked(true);
break;
}
// Remove all listeners
holder.itemView.setOnClickListener(null);
holder.checkBox.setOnCheckedChangeListener(null);
if (SNLIST.contains(info.packageName)) {
holder.checkBox.setChecked(true);
holder.checkBox.setEnabled(false);
holder.itemView.setOnClickListener(v ->
SnackbarMaker.make(holder.itemView,
R.string.safetyNet_hide_notice, Snackbar.LENGTH_LONG).show()
);
} else {
holder.checkBox.setEnabled(true);
holder.checkBox.setChecked(mHideList.contains(info.packageName));
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
if (isChecked) {
Utils.addMagiskHide(shell, info.packageName);
mHideList.add(info.packageName);
} else {
Utils.rmMagiskHide(shell, info.packageName);
mHideList.remove(info.packageName);
}
});
}
holder.checkBox.setOnClickListener(v -> {
CheckBox chkbox = (CheckBox) v;
if (chkbox.isChecked()) {
new Async.MagiskHide().add(info.packageName);
mHideList.add(info.packageName);
}
else {
new Async.MagiskHide().rm(info.packageName);
mHideList.remove(info.packageName);
}
});
}
@Override
@@ -87,19 +104,72 @@ public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.
return mList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
public void filter(String constraint) {
filter.filter(constraint);
}
public void refresh() {
new LoadApps().exec();
}
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.app_icon) ImageView appIcon;
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.app_package) TextView appPackage;
@BindView(R.id.checkbox) CheckBox checkBox;
public ViewHolder(View itemView) {
ViewHolder(View itemView) {
super(itemView);
WindowManager windowmanager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
ButterKnife.bind(this, itemView);
DisplayMetrics dimension = new DisplayMetrics();
windowmanager.getDefaultDisplay().getMetrics(dimension);
}
}
}
private class ApplicationFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
if (constraint == null || constraint.length() == 0) {
mList = mOriginalList;
} else {
mList = new ArrayList<>();
String filter = constraint.toString().toLowerCase();
for (ApplicationInfo info : mOriginalList) {
if (Utils.lowercaseContains(info.loadLabel(pm), filter)
|| Utils.lowercaseContains(info.packageName, filter)) {
mList.add(info);
}
}
}
return null;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
notifyDataSetChanged();
}
}
private class LoadApps extends ParallelTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
mOriginalList = pm.getInstalledApplications(0);
for (Iterator<ApplicationInfo> i = mOriginalList.iterator(); i.hasNext(); ) {
ApplicationInfo info = i.next();
if (ApplicationAdapter.BLACKLIST.contains(info.packageName) || !info.enabled) {
i.remove();
}
}
Collections.sort(mOriginalList, (a, b) -> a.loadLabel(pm).toString().toLowerCase()
.compareTo(b.loadLabel(pm).toString().toLowerCase()));
mHideList = Utils.listMagiskHide(shell);
return null;
}
@Override
protected void onPostExecute(Void v) {
magiskHideDone.publish(false);
}
}
}

View File

@@ -3,18 +3,17 @@ package com.topjohnwu.magisk.adapters;
import android.content.Context;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.module.Module;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.components.SnackbarMaker;
import com.topjohnwu.magisk.container.Module;
import com.topjohnwu.magisk.utils.Shell;
import java.util.List;
@@ -25,7 +24,6 @@ import butterknife.ButterKnife;
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
private final List<Module> mList;
private Context context;
public ModulesAdapter(List<Module> list) {
mList = list;
@@ -34,90 +32,51 @@ public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHold
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
context = parent.getContext();
ButterKnife.bind(this, view);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
Context context = holder.itemView.getContext();
Shell rootShell = Shell.getShell(context);
final Module module = mList.get(position);
holder.title.setText(module.getName());
String version = module.getVersion();
String author = module.getAuthor();
String versionName = module.getVersion();
String description = module.getDescription();
if (versionName != null) {
holder.versionName.setText(versionName);
}
if (author != null) {
holder.author.setText(context.getString(R.string.author, author));
}
if (description != null) {
holder.description.setText(description);
}
String noInfo = context.getString(R.string.no_info_provided);
holder.title.setText(module.getName());
holder.versionName.setText( TextUtils.isEmpty(version) ? noInfo : version);
holder.author.setText( TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
holder.description.setText( TextUtils.isEmpty(description) ? noInfo : description);
holder.checkBox.setOnCheckedChangeListener(null);
holder.checkBox.setChecked(module.isEnabled());
holder.checkBox.setOnClickListener((v) -> {
CheckBox checkBox = (CheckBox) v;
if (checkBox.isChecked()) {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
module.removeDisableFile();
return null;
}
@Override
protected void onPostExecute(Void v) {
Snackbar.make(holder.title, R.string.disable_file_removed, Snackbar.LENGTH_SHORT).show();
}
}.exec();
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
int snack;
if (isChecked) {
module.removeDisableFile(rootShell);
snack = R.string.disable_file_removed;
} else {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
module.createDisableFile();
return null;
}
@Override
protected void onPostExecute(Void v) {
Snackbar.make(holder.title, R.string.disable_file_created, Snackbar.LENGTH_SHORT).show();
}
}.exec();
module.createDisableFile(rootShell);
snack = R.string.disable_file_created;
}
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
});
holder.delete.setOnClickListener(v -> {
if (module.willBeRemoved()) {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
module.deleteRemoveFile();
return null;
}
@Override
protected void onPostExecute(Void v) {
Snackbar.make(holder.title, R.string.remove_file_deleted, Snackbar.LENGTH_SHORT).show();
updateDeleteButton(holder, module);
}
}.exec();
boolean removed = module.willBeRemoved();
int snack;
if (removed) {
module.deleteRemoveFile(rootShell);
snack = R.string.remove_file_deleted;
} else {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
module.createRemoveFile();
return null;
}
@Override
protected void onPostExecute(Void v) {
Snackbar.make(holder.title, R.string.remove_file_created, Snackbar.LENGTH_SHORT).show();
updateDeleteButton(holder, module);
}
}.exec();
module.createRemoveFile(rootShell);
snack = R.string.remove_file_created;
}
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
updateDeleteButton(holder, module);
});
if (module.isUpdated()) {
@@ -144,7 +103,7 @@ public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHold
return mList.size();
}
class ViewHolder extends RecyclerView.ViewHolder {
static class ViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.title) TextView title;
@BindView(R.id.version_name) TextView versionName;
@@ -154,12 +113,9 @@ public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHold
@BindView(R.id.author) TextView author;
@BindView(R.id.delete) ImageView delete;
public ViewHolder(View itemView) {
ViewHolder(View itemView) {
super(itemView);
WindowManager windowmanager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
ButterKnife.bind(this, itemView);
DisplayMetrics dimension = new DisplayMetrics();
windowmanager.getDefaultDisplay().getMetrics(dimension);
if (!Shell.rootAccess()) {
checkBox.setEnabled(false);

View File

@@ -0,0 +1,150 @@
package com.topjohnwu.magisk.adapters;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Switch;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.components.AlertDialogBuilder;
import com.topjohnwu.magisk.components.ExpandableView;
import com.topjohnwu.magisk.components.SnackbarMaker;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.database.SuDatabaseHelper;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import butterknife.BindView;
import butterknife.ButterKnife;
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
private List<Policy> policyList;
private SuDatabaseHelper dbHelper;
private PackageManager pm;
private Set<Policy> expandList = new HashSet<>();
public PolicyAdapter(List<Policy> list, SuDatabaseHelper db, PackageManager pm) {
policyList = list;
dbHelper = db;
this.pm = pm;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
Policy policy = policyList.get(position);
holder.setExpanded(expandList.contains(policy));
holder.itemView.setOnClickListener(view -> {
if (holder.isExpanded()) {
holder.collapse();
expandList.remove(policy);
} else {
holder.expand();
expandList.add(policy);
}
});
holder.appName.setText(policy.appName);
holder.packageName.setText(policy.packageName);
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
holder.masterSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && policy.policy == Policy.DENY) ||
(!isChecked && policy.policy == Policy.ALLOW)) {
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && !policy.notification) ||
(!isChecked && policy.notification)) {
policy.notification = isChecked;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
if ((isChecked && !policy.logging) ||
(!isChecked && policy.logging)) {
policy.logging = isChecked;
String message = v.getContext().getString(
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
dbHelper.updatePolicy(policy);
}
});
holder.delete.setOnClickListener(v -> new AlertDialogBuilder((Activity) v.getContext())
.setTitle(R.string.su_revoke_title)
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
.setPositiveButton(R.string.yes, (dialog, which) -> {
policyList.remove(position);
notifyItemRemoved(position);
notifyItemRangeChanged(position, policyList.size());
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
Snackbar.LENGTH_SHORT).show();
dbHelper.deletePolicy(policy);
})
.setNegativeButton(R.string.no_thanks, null)
.setCancelable(true)
.show());
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
holder.notificationSwitch.setChecked(policy.notification);
holder.loggingSwitch.setChecked(policy.logging);
// Hide for now
holder.moreInfo.setVisibility(View.GONE);
}
@Override
public int getItemCount() {
return policyList.size();
}
static class ViewHolder extends RecyclerView.ViewHolder implements ExpandableView {
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.package_name) TextView packageName;
@BindView(R.id.app_icon) ImageView appIcon;
@BindView(R.id.master_switch) Switch masterSwitch;
@BindView(R.id.notification_switch) Switch notificationSwitch;
@BindView(R.id.logging_switch) Switch loggingSwitch;
@BindView(R.id.expand_layout) ViewGroup expandLayout;
@BindView(R.id.delete) ImageView delete;
@BindView(R.id.more_info) ImageView moreInfo;
private Container container = new Container();
public ViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
container.expandLayout = expandLayout;
setupExpandable();
}
@Override
public Container getContainer() {
return container;
}
}
}

View File

@@ -1,218 +1,189 @@
package com.topjohnwu.magisk.adapters;
import android.animation.Animator;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.text.TextUtils;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.topjohnwu.magisk.MainActivity;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.module.Repo;
import com.topjohnwu.magisk.receivers.RepoDlReceiver;
import com.topjohnwu.magisk.asyncs.MarkDownWindow;
import com.topjohnwu.magisk.asyncs.ProcessRepoZip;
import com.topjohnwu.magisk.components.AlertDialogBuilder;
import com.topjohnwu.magisk.container.Module;
import com.topjohnwu.magisk.container.Repo;
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.WebWindow;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import butterknife.BindView;
import butterknife.ButterKnife;
public class ReposAdapter extends RecyclerView.Adapter<ReposAdapter.ViewHolder> {
public class ReposAdapter extends SectionedAdapter<ReposAdapter.SectionHolder, ReposAdapter.RepoHolder> {
private List<Repo> mUpdateRepos, mInstalledRepos, mOthersRepos;
private View mView;
private Context mContext;
private static final int UPDATES = 0;
private static final int INSTALLED = 1;
private static final int OTHERS = 2;
public ReposAdapter(List<Repo> update, List<Repo> installed, List<Repo> others) {
mUpdateRepos = update;
mInstalledRepos = installed;
mOthersRepos = others;
private Cursor repoCursor = null;
private Map<String, Module> moduleMap;
private RepoDatabaseHelper repoDB;
private List<Pair<Integer, List<Repo>>> repoPairs;
public ReposAdapter(RepoDatabaseHelper db, Map<String, Module> map) {
repoDB = db;
moduleMap = map;
repoPairs = new ArrayList<>();
notifyDBChanged();
}
@Override
public int getSectionCount() {
return repoPairs.size();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
ButterKnife.bind(this, mView);
mContext = parent.getContext();
return new ViewHolder(mView);
public int getItemCount(int section) {
return repoPairs.get(section).second.size();
}
@Override
public void onBindViewHolder(final ViewHolder holder, int position) {
final Repo repo;
if (position >= mUpdateRepos.size()) {
position -= mUpdateRepos.size();
if (position >= mInstalledRepos.size()) {
position -= mInstalledRepos.size();
repo = mOthersRepos.get(position);
} else {
repo = mInstalledRepos.get(position);
}
} else {
repo = mUpdateRepos.get(position);
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.section, parent, false);
return new SectionHolder(v);
}
@Override
public RepoHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
return new RepoHolder(v);
}
@Override
public void onBindSectionViewHolder(SectionHolder holder, int section) {
switch (repoPairs.get(section).first) {
case UPDATES:
holder.sectionText.setText(R.string.update_available);
break;
case INSTALLED:
holder.sectionText.setText(R.string.installed);
break;
case OTHERS:
holder.sectionText.setText(R.string.not_installed);
break;
}
}
@Override
public void onBindItemViewHolder(RepoHolder holder, int section, int position) {
Repo repo = repoPairs.get(section).second.get(position);
Context context = holder.itemView.getContext();
holder.title.setText(repo.getName());
holder.versionName.setText(repo.getVersion());
String author = repo.getAuthor();
String versionName = repo.getVersion();
String description = repo.getDescription();
if (versionName != null) {
holder.versionName.setText(versionName);
}
if (author != null) {
holder.author.setText(mContext.getString(R.string.author, author));
}
if (description != null) {
holder.description.setText(description);
}
holder.author.setText(TextUtils.isEmpty(author) ? null : context.getString(R.string.author, author));
holder.description.setText(repo.getDescription());
View.OnClickListener listener = view -> {
if (view.getId() == holder.updateImage.getId()) {
String filename = repo.getName() + "-" + repo.getVersion() + ".zip";
MainActivity.alertBuilder
.setTitle(mContext.getString(R.string.repo_install_title, repo.getName()))
.setMessage(mContext.getString(R.string.repo_install_msg, filename))
.setCancelable(true)
.setPositiveButton(R.string.download_install, (dialogInterface, i) -> Utils.dlAndReceive(
mContext,
new RepoDlReceiver(),
repo.getZipUrl(),
Utils.getLegalFilename(filename)))
.setNegativeButton(R.string.no_thanks, null)
.show();
}
if ((view.getId() == holder.changeLog.getId()) && (!repo.getLogUrl().equals(""))) {
new WebWindow(mContext.getString(R.string.changelog), repo.getLogUrl(), mContext);
}
if ((view.getId() == holder.authorLink.getId()) && (!repo.getSupportUrl().equals(""))) {
mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(repo.getDonateUrl())));
}
if ((view.getId() == holder.supportLink.getId()) && (!repo.getSupportUrl().equals(""))) {
mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(repo.getSupportUrl())));
}
};
holder.infoLayout.setOnClickListener(v ->
new MarkDownWindow((Activity) context, null, repo.getDetailUrl()).exec());
holder.changeLog.setOnClickListener(listener);
holder.updateImage.setOnClickListener(listener);
holder.authorLink.setOnClickListener(listener);
holder.supportLink.setOnClickListener(listener);
holder.downloadImage.setOnClickListener(v -> {
String filename = repo.getName() + "-" + repo.getVersion() + ".zip";
new AlertDialogBuilder((Activity) context)
.setTitle(context.getString(R.string.repo_install_title, repo.getName()))
.setMessage(context.getString(R.string.repo_install_msg, filename))
.setCancelable(true)
.setPositiveButton(R.string.install, (d, i) ->
new ProcessRepoZip((Activity) context, repo.getZipUrl(),
Utils.getLegalFilename(filename), true).exec()
)
.setNeutralButton(R.string.download, (d, i) ->
new ProcessRepoZip((Activity) context, repo.getZipUrl(),
Utils.getLegalFilename(filename), false).exec())
.setNegativeButton(R.string.no_thanks, null)
.show();
});
}
@Override
public int getItemCount() {
return mUpdateRepos.size() + mInstalledRepos.size() + mOthersRepos.size();
public void notifyDBChanged() {
if (repoCursor != null)
repoCursor.close();
repoCursor = repoDB.getRepoCursor();
filter("");
}
class ViewHolder extends RecyclerView.ViewHolder {
public void filter(String s) {
List<Repo> updates = new ArrayList<>();
List<Repo> installed = new ArrayList<>();
List<Repo> others = new ArrayList<>();
repoPairs.clear();
while (repoCursor.moveToNext()) {
Repo repo = new Repo(repoCursor);
if (repo.getName().toLowerCase().contains(s.toLowerCase())
|| repo.getAuthor().toLowerCase().contains(s.toLowerCase())
|| repo.getDescription().toLowerCase().contains(s.toLowerCase())
) {
// Passed the repoFilter
Module module = moduleMap.get(repo.getId());
if (module != null) {
if (repo.getVersionCode() > module.getVersionCode()) {
// Updates
updates.add(repo);
} else {
installed.add(repo);
}
} else {
others.add(repo);
}
}
}
repoCursor.moveToFirst();
if (!updates.isEmpty())
repoPairs.add(new Pair<>(UPDATES, updates));
if (!installed.isEmpty())
repoPairs.add(new Pair<>(INSTALLED, installed));
if (!others.isEmpty())
repoPairs.add(new Pair<>(OTHERS, others));
notifyDataSetChanged();
}
static class SectionHolder extends RecyclerView.ViewHolder {
@BindView(R.id.section_text) TextView sectionText;
SectionHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
static class RepoHolder extends RecyclerView.ViewHolder {
@BindView(R.id.title) TextView title;
@BindView(R.id.version_name) TextView versionName;
@BindView(R.id.description) TextView description;
@BindView(R.id.author) TextView author;
@BindView(R.id.expand_layout) LinearLayout expandLayout;
@BindView(R.id.update) ImageView updateImage;
@BindView(R.id.installed) ImageView installedImage;
@BindView(R.id.changeLog) ImageView changeLog;
@BindView(R.id.authorLink) ImageView authorLink;
@BindView(R.id.supportLink) ImageView supportLink;
@BindView(R.id.info_layout) LinearLayout infoLayout;
@BindView(R.id.download) ImageView downloadImage;
private ValueAnimator mAnimator;
private ObjectAnimator animY2;
private ViewHolder holder;
private boolean expanded = false;
public ViewHolder(View itemView) {
RepoHolder(View itemView) {
super(itemView);
WindowManager windowmanager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
ButterKnife.bind(this, itemView);
DisplayMetrics dimension = new DisplayMetrics();
windowmanager.getDefaultDisplay().getMetrics(dimension);
holder = this;
this.expandLayout.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
holder.expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
holder.expandLayout.setVisibility(View.GONE);
holder.expandLayout.measure(widthSpec, heightSpec);
final int holderHeight = holder.expandLayout.getMeasuredHeight();
mAnimator = slideAnimator(0, holderHeight);
animY2 = ObjectAnimator.ofFloat(holder.updateImage, "translationY", holderHeight / 2);
return true;
}
});
mView.setOnClickListener(view -> {
if (expanded) {
collapse(holder.expandLayout);
} else {
expand(holder.expandLayout);
}
expanded = !expanded;
});
}
private void expand(View view) {
view.setVisibility(View.VISIBLE);
mAnimator.start();
animY2.start();
}
private void collapse(View view) {
int finalHeight = view.getHeight();
ValueAnimator mAnimator = slideAnimator(finalHeight, 0);
mAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.GONE);
}
@Override
public void onAnimationStart(Animator animator) {}
@Override
public void onAnimationCancel(Animator animator) {}
@Override
public void onAnimationRepeat(Animator animator) {}
});
mAnimator.start();
animY2.reverse();
}
private ValueAnimator slideAnimator(int start, int end) {
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.addUpdateListener(valueAnimator -> {
int value = (Integer) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams layoutParams = expandLayout
.getLayoutParams();
layoutParams.height = value;
expandLayout.setLayoutParams(layoutParams);
});
return animator;
}
}

View File

@@ -0,0 +1,93 @@
package com.topjohnwu.magisk.adapters;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
public abstract class SectionedAdapter<S extends RecyclerView.ViewHolder, C extends RecyclerView.ViewHolder>
extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int SECTION_TYPE = Integer.MIN_VALUE;
@Override
final public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == SECTION_TYPE)
return onCreateSectionViewHolder(parent);
return onCreateItemViewHolder(parent, viewType);
}
@Override
@SuppressWarnings("unchecked")
final public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
PositionInfo info = getPositionInfo(position);
if (info.position == -1)
onBindSectionViewHolder((S) holder, info.section);
else
onBindItemViewHolder((C) holder, info.section, info.position);
}
@Override
final public int getItemCount() {
int size, sec;
size = sec = getSectionCount();
for (int i = 0; i < sec; ++i){
size += getItemCount(i);
}
return size;
}
@Override
final public int getItemViewType(int position) {
PositionInfo info = getPositionInfo(position);
if (info.position == -1)
return SECTION_TYPE;
else
return getItemViewType(info.section, info.position);
}
public int getItemViewType(int section, int position) {
return 0;
}
protected int getSectionPosition(int section) {
return getItemPosition(section, -1);
}
protected int getItemPosition(int section, int position) {
int realPosition = 0;
// Previous sections
for (int i = 0; i < section; ++i) {
realPosition += getItemCount(i) + 1;
}
// Current section
realPosition += position + 1;
return realPosition;
}
private PositionInfo getPositionInfo(int position) {
int section = 0;
while (true) {
if (position == 0)
return new PositionInfo(section, -1);
position -= 1;
if (position < getItemCount(section))
return new PositionInfo(section, position);
position -= getItemCount(section++);
}
}
private static class PositionInfo {
int section;
int position;
PositionInfo(int section, int position) {
this.section = section;
this.position = position;
}
}
public abstract int getSectionCount();
public abstract int getItemCount(int section);
public abstract S onCreateSectionViewHolder(ViewGroup parent);
public abstract C onCreateItemViewHolder(ViewGroup parent, int viewType);
public abstract void onBindSectionViewHolder(S holder, int section);
public abstract void onBindItemViewHolder(C holder, int section, int position);
}

View File

@@ -1,183 +0,0 @@
package com.topjohnwu.magisk.adapters;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.Arrays;
import java.util.Comparator;
public class SimpleSectionedRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private final Context mContext;
private static final int SECTION_TYPE = 0;
private boolean mValid = true;
private int mSectionResourceId;
private int mTextResourceId;
private LayoutInflater mLayoutInflater;
private RecyclerView.Adapter mBaseAdapter;
private SparseArray<Section> mSections = new SparseArray<Section>();
public SimpleSectionedRecyclerViewAdapter(Context context, int sectionResourceId, int textResourceId,
RecyclerView.Adapter baseAdapter) {
mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mSectionResourceId = sectionResourceId;
mTextResourceId = textResourceId;
mBaseAdapter = baseAdapter;
mContext = context;
mBaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
mValid = mBaseAdapter.getItemCount()>0;
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(int positionStart, int itemCount) {
mValid = mBaseAdapter.getItemCount()>0;
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
mValid = mBaseAdapter.getItemCount()>0;
notifyItemRangeInserted(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
mValid = mBaseAdapter.getItemCount()>0;
notifyItemRangeRemoved(positionStart, itemCount);
}
});
}
public static class SectionViewHolder extends RecyclerView.ViewHolder {
public TextView title;
public SectionViewHolder(View view, int mTextResourceid) {
super(view);
title = (TextView) view.findViewById(mTextResourceid);
}
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int typeView) {
if (typeView == SECTION_TYPE) {
final View view = LayoutInflater.from(mContext).inflate(mSectionResourceId, parent, false);
return new SectionViewHolder(view,mTextResourceId);
}else{
return mBaseAdapter.onCreateViewHolder(parent, typeView -1);
}
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder sectionViewHolder, int position) {
if (isSectionHeaderPosition(position)) {
((SectionViewHolder)sectionViewHolder).title.setText(mSections.get(position).title);
}else{
mBaseAdapter.onBindViewHolder(sectionViewHolder,sectionedPositionToPosition(position));
}
}
@Override
public int getItemViewType(int position) {
return isSectionHeaderPosition(position)
? SECTION_TYPE
: mBaseAdapter.getItemViewType(sectionedPositionToPosition(position)) +1 ;
}
public static class Section {
int firstPosition;
int sectionedPosition;
CharSequence title;
public Section(int firstPosition, CharSequence title) {
this.firstPosition = firstPosition;
this.title = title;
}
public CharSequence getTitle() {
return title;
}
}
public void setSections(Section[] sections) {
mSections.clear();
Arrays.sort(sections, new Comparator<Section>() {
@Override
public int compare(Section o, Section o1) {
return (o.firstPosition == o1.firstPosition)
? 0
: ((o.firstPosition < o1.firstPosition) ? -1 : 1);
}
});
int offset = 0; // offset positions for the headers we're adding
for (Section section : sections) {
section.sectionedPosition = section.firstPosition + offset;
mSections.append(section.sectionedPosition, section);
++offset;
}
notifyDataSetChanged();
}
public int positionToSectionedPosition(int position) {
int offset = 0;
for (int i = 0; i < mSections.size(); i++) {
if (mSections.valueAt(i).firstPosition > position) {
break;
}
++offset;
}
return position + offset;
}
public int sectionedPositionToPosition(int sectionedPosition) {
if (isSectionHeaderPosition(sectionedPosition)) {
return RecyclerView.NO_POSITION;
}
int offset = 0;
for (int i = 0; i < mSections.size(); i++) {
if (mSections.valueAt(i).sectionedPosition > sectionedPosition) {
break;
}
--offset;
}
return sectionedPosition + offset;
}
public boolean isSectionHeaderPosition(int position) {
return mSections.get(position) != null;
}
@Override
public long getItemId(int position) {
return isSectionHeaderPosition(position)
? Integer.MAX_VALUE - mSections.indexOfKey(position)
: mBaseAdapter.getItemId(sectionedPositionToPosition(position));
}
@Override
public int getItemCount() {
return (mValid ? mBaseAdapter.getItemCount() + mSections.size() : 0);
}
}

View File

@@ -0,0 +1,155 @@
package com.topjohnwu.magisk.adapters;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.components.ExpandableView;
import com.topjohnwu.magisk.container.SuLogEntry;
import com.topjohnwu.magisk.database.SuDatabaseHelper;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SuLogAdapter extends SectionedAdapter<SuLogAdapter.SectionHolder, SuLogAdapter.LogViewHolder> {
private List<List<Integer>> logEntryList;
private Set<Integer> itemExpanded, sectionExpanded;
private SuDatabaseHelper suDB;
private Cursor suLogCursor = null;
public SuLogAdapter(SuDatabaseHelper db) {
suDB = db;
logEntryList = Collections.emptyList();
sectionExpanded = new HashSet<>();
itemExpanded = new HashSet<>();
}
@Override
public int getSectionCount() {
return logEntryList.size();
}
@Override
public int getItemCount(int section) {
return sectionExpanded.contains(section) ? logEntryList.get(section).size() : 0;
}
@Override
public SectionHolder onCreateSectionViewHolder(ViewGroup parent) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
return new SectionHolder(v);
}
@Override
public LogViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
return new LogViewHolder(v);
}
@Override
public void onBindSectionViewHolder(SectionHolder holder, int section) {
suLogCursor.moveToPosition(logEntryList.get(section).get(0));
SuLogEntry entry = new SuLogEntry(suLogCursor);
holder.arrow.setRotation(sectionExpanded.contains(section) ? 180 : 0);
holder.itemView.setOnClickListener(v -> {
RotateAnimation rotate;
if (sectionExpanded.contains(section)) {
holder.arrow.setRotation(0);
rotate = new RotateAnimation(180, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sectionExpanded.remove(section);
notifyItemRangeRemoved(getItemPosition(section, 0), logEntryList.get(section).size());
} else {
rotate = new RotateAnimation(0, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
sectionExpanded.add(section);
notifyItemRangeInserted(getItemPosition(section, 0), logEntryList.get(section).size());
}
rotate.setDuration(300);
rotate.setFillAfter(true);
holder.arrow.setAnimation(rotate);
});
holder.date.setText(entry.getDateString());
}
@Override
public void onBindItemViewHolder(LogViewHolder holder, int section, int position) {
int sqlPosition = logEntryList.get(section).get(position);
suLogCursor.moveToPosition(sqlPosition);
SuLogEntry entry = new SuLogEntry(suLogCursor);
holder.setExpanded(itemExpanded.contains(sqlPosition));
holder.itemView.setOnClickListener(view -> {
if (holder.isExpanded()) {
holder.collapse();
itemExpanded.remove(sqlPosition);
} else {
holder.expand();
itemExpanded.add(sqlPosition);
}
});
holder.appName.setText(entry.appName);
holder.action.setText(entry.action ? R.string.grant : R.string.deny);
holder.command.setText(entry.command);
holder.fromPid.setText(String.valueOf(entry.fromPid));
holder.toUid.setText(String.valueOf(entry.toUid));
holder.time.setText(entry.getTimeString());
}
public void notifyDBChanged() {
if (suLogCursor != null)
suLogCursor.close();
suLogCursor = suDB.getLogCursor();
logEntryList = suDB.getLogStructure();
itemExpanded.clear();
sectionExpanded.clear();
sectionExpanded.add(0);
notifyDataSetChanged();
}
static class SectionHolder extends RecyclerView.ViewHolder {
@BindView(R.id.date) TextView date;
@BindView(R.id.arrow) ImageView arrow;
SectionHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
}
static class LogViewHolder extends RecyclerView.ViewHolder implements ExpandableView {
@BindView(R.id.app_name) TextView appName;
@BindView(R.id.action) TextView action;
@BindView(R.id.time) TextView time;
@BindView(R.id.fromPid) TextView fromPid;
@BindView(R.id.toUid) TextView toUid;
@BindView(R.id.command) TextView command;
@BindView(R.id.expand_layout) ViewGroup expandLayout;
private Container container = new Container();
LogViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
container.expandLayout = expandLayout;
setupExpandable();
}
@Override
public Container getContainer() {
return container;
}
}
}

View File

@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.adapters;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
public class TabFragmentAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList;
private List<String> titleList;
public TabFragmentAdapter(FragmentManager fm) {
super(fm);
fragmentList = new ArrayList<>();
titleList = new ArrayList<>();
}
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
@Override
public int getCount() {
return fragmentList.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titleList.get(position);
}
public void addTab(Fragment fragment, String title) {
fragmentList.add(fragment);
titleList.add(title);
}
}

View File

@@ -0,0 +1,84 @@
package com.topjohnwu.magisk.asyncs;
import android.support.v4.app.FragmentActivity;
import com.topjohnwu.jarsigner.ByteArrayStream;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.utils.WebService;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Proxy;
import java.net.HttpURLConnection;
import dalvik.system.DexClassLoader;
public class CheckSafetyNet extends ParallelTask<Void, Void, Exception> {
public static final int SNET_VER = 2;
private static final String SNET_URL = "https://github.com/topjohnwu/MagiskManager/releases/download/v5.4.0/snet.apk";
private static final String PKG = "com.topjohnwu.snet";
private File dexPath;
private DexClassLoader loader;
public CheckSafetyNet(FragmentActivity activity) {
super(activity);
dexPath = new File(activity.getCacheDir().getParent() + "/snet", "snet.apk");
}
@Override
protected void onPreExecute() {
MagiskManager mm = getMagiskManager();
if (mm.snet_version != CheckSafetyNet.SNET_VER) {
getShell().sh("rm -rf " + dexPath.getParent());
}
mm.snet_version = CheckSafetyNet.SNET_VER;
mm.prefs.edit().putInt("snet_version", CheckSafetyNet.SNET_VER).apply();
}
@Override
protected Exception doInBackground(Void... voids) {
try {
if (!dexPath.exists()) {
HttpURLConnection conn = WebService.request(SNET_URL, null);
ByteArrayStream bas = new ByteArrayStream();
bas.readFrom(conn.getInputStream());
conn.disconnect();
dexPath.getParentFile().mkdir();
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(dexPath))) {
bas.writeTo(out);
out.flush();
}
}
loader = new DexClassLoader(dexPath.toString(), dexPath.getParent(),
null, ClassLoader.getSystemClassLoader());
} catch (Exception e) {
return e;
}
return null;
}
@Override
protected void onPostExecute(Exception err) {
try {
if (err != null) throw err;
Class<?> helperClazz = loader.loadClass(PKG + ".SafetyNetHelper");
Class<?> callbackClazz = loader.loadClass(PKG + ".SafetyNetCallback");
Object helper = helperClazz.getConstructors()[0].newInstance(
getActivity(), Proxy.newProxyInstance(
loader, new Class[] { callbackClazz }, (proxy, method, args) -> {
getMagiskManager().safetyNetDone.publish(false, args[0]);
return null;
}));
helperClazz.getMethod("attest").invoke(helper);
} catch (Exception e) {
e.printStackTrace();
getMagiskManager().safetyNetDone.publish(false, -1);
}
super.onPostExecute(err);
}
}

View File

@@ -0,0 +1,76 @@
package com.topjohnwu.magisk.asyncs;
import android.content.Context;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.WebService;
import org.json.JSONException;
import org.json.JSONObject;
public class CheckUpdates extends ParallelTask<Void, Void, Void> {
public static final int STABLE_CHANNEL = 0;
public static final int BETA_CHANNEL = 1;
private static final String STABLE_URL = "https://raw.githubusercontent.com/topjohnwu/MagiskManager/update/stable.json";
private static final String BETA_URL = "https://raw.githubusercontent.com/topjohnwu/MagiskManager/update/beta.json";
private boolean showNotification = false;
public CheckUpdates(Context context) {
super(context);
}
public CheckUpdates(Context context, boolean b) {
super(context);
showNotification = b;
}
@Override
protected Void doInBackground(Void... voids) {
MagiskManager mm = getMagiskManager();
if (mm == null) return null;
String jsonStr;
switch (mm.updateChannel) {
case STABLE_CHANNEL:
jsonStr = WebService.getString(STABLE_URL);
break;
case BETA_CHANNEL:
jsonStr = WebService.getString(BETA_URL);
break;
default:
jsonStr = null;
}
try {
JSONObject json = new JSONObject(jsonStr);
JSONObject magisk = json.getJSONObject("magisk");
mm.remoteMagiskVersionString = magisk.getString("version");
mm.remoteMagiskVersionCode = magisk.getInt("versionCode");
mm.magiskLink = magisk.getString("link");
mm.releaseNoteLink = magisk.getString("note");
JSONObject manager = json.getJSONObject("app");
mm.remoteManagerVersionString = manager.getString("version");
mm.remoteManagerVersionCode = manager.getInt("versionCode");
mm.managerLink = manager.getString("link");
} catch (JSONException ignored) {}
return null;
}
@Override
protected void onPostExecute(Void v) {
MagiskManager mm = getMagiskManager();
if (mm == null) return;
if (showNotification && mm.updateNotification) {
if (BuildConfig.VERSION_CODE < mm.remoteManagerVersionCode) {
Utils.showManagerUpdateNotification(mm);
} else if (mm.magiskVersionCode < mm.remoteMagiskVersionCode) {
Utils.showMagiskUpdateNotification(mm);
}
}
mm.updateCheckDone.publish();
super.onPostExecute(v);
}
}

View File

@@ -0,0 +1,64 @@
package com.topjohnwu.magisk.asyncs;
import android.content.Context;
import android.os.Build;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.WebService;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
public class DownloadBusybox extends ParallelTask<Void, Void, Void> {
private static final String BUSYBOX_ARM = "https://github.com/topjohnwu/ndk-busybox/releases/download/1.27.2/busybox-arm";
private static final String BUSYBOX_X86 = "https://github.com/topjohnwu/ndk-busybox/releases/download/1.27.2/busybox-x86";
private File busybox;
public DownloadBusybox(Context context) {
super(context);
busybox = new File(context.getCacheDir(), "busybox");
}
@Override
protected Void doInBackground(Void... voids) {
Context context = getMagiskManager();
Utils.removeItem(getShell(), context.getApplicationInfo().dataDir + "/busybox");
try {
FileOutputStream out = new FileOutputStream(busybox);
HttpURLConnection conn = WebService.request(
Build.SUPPORTED_32_BIT_ABIS[0].contains("x86") ?
BUSYBOX_X86 :
BUSYBOX_ARM,
null
);
if (conn == null) throw new IOException();
BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
byte[] buffer = new byte[4096];
int len;
while ((len = bis.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
out.close();
conn.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
if (busybox.exists()) {
getShell().su(
"rm -rf " + MagiskManager.BUSYBOXPATH,
"mkdir -p " + MagiskManager.BUSYBOXPATH,
"cp " + busybox + " " + MagiskManager.BUSYBOXPATH,
"chmod -R 755 " + MagiskManager.BUSYBOXPATH,
MagiskManager.BUSYBOXPATH + "/busybox --install -s " + MagiskManager.BUSYBOXPATH
);
busybox.delete();
}
return null;
}
}

View File

@@ -0,0 +1,117 @@
package com.topjohnwu.magisk.asyncs;
import android.app.Activity;
import android.net.Uri;
import android.text.TextUtils;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.container.AdaptiveList;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.ZipUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
public class FlashZip extends ParallelTask<Void, Void, Integer> {
private Uri mUri;
private File mCachedFile;
private AdaptiveList<String> mList;
public FlashZip(Activity context, Uri uri, AdaptiveList<String> list) {
super(context);
mUri = uri;
mList = list;
mCachedFile = new File(context.getCacheDir(), "install.zip");
}
private boolean unzipAndCheck() throws Exception {
ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android", true);
List<String> ret = Utils.readFile(getShell(), new File(mCachedFile.getParentFile(), "updater-script").getPath());
return Utils.isValidShellResponse(ret) && ret.get(0).contains("#MAGISK");
}
@Override
protected void onPreExecute() {
// UI updates must run in the UI thread
mList.setCallback(this::publishProgress);
}
@Override
protected void onProgressUpdate(Void... values) {
mList.updateView();
}
@Override
protected Integer doInBackground(Void... voids) {
MagiskManager mm = getMagiskManager();
if (mm == null) return -1;
try {
mList.add("- Copying zip to temp directory");
mCachedFile.delete();
try (
InputStream in = mm.getContentResolver().openInputStream(mUri);
OutputStream out = new BufferedOutputStream(new FileOutputStream(mCachedFile))
) {
if (in == null) throw new FileNotFoundException();
InputStream buf= new BufferedInputStream(in);
byte buffer[] = new byte[4096];
int length;
while ((length = buf.read(buffer)) > 0)
out.write(buffer, 0, length);
} catch (FileNotFoundException e) {
mList.add("! Invalid Uri");
throw e;
} catch (IOException e) {
mList.add("! Cannot copy to cache");
throw e;
}
if (!unzipAndCheck()) return 0;
mList.add("- Installing " + Utils.getNameFromUri(mm, mUri));
getShell().su(mList,
"cd " + mCachedFile.getParent(),
"BOOTMODE=true sh update-binary dummy 1 " + mCachedFile +
" && echo 'Success!' || echo 'Failed!'"
);
if (TextUtils.equals(mList.get(mList.size() - 1), "Success!"))
return 1;
} catch (Exception e) {
e.printStackTrace();
}
return -1;
}
// -1 = error, manual install; 0 = invalid zip; 1 = success
@Override
protected void onPostExecute(Integer result) {
MagiskManager mm = getMagiskManager();
if (mm == null) return;
getShell().su_raw(
"rm -rf " + mCachedFile.getParent(),
"rm -rf " + MagiskManager.TMP_FOLDER_PATH
);
switch (result) {
case -1:
mList.add(mm.getString(R.string.install_error));
Utils.showUriSnack(getActivity(), mUri);
break;
case 0:
mList.add(mm.getString(R.string.invalid_zip));
break;
case 1:
// Success
new LoadModules(mm).exec();
break;
}
super.onPostExecute(result);
}
}

View File

@@ -0,0 +1,115 @@
package com.topjohnwu.magisk.asyncs;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.widget.Toast;
import com.topjohnwu.jarsigner.JarMap;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.ZipUtils;
import java.io.File;
import java.util.List;
import java.util.jar.JarEntry;
public class HideManager extends ParallelTask<Void, Void, Boolean> {
private static final String UNHIDE_APK = "unhide.apk";
private static final String ANDROID_MANIFEST = "AndroidManifest.xml";
private static final byte[] UNHIDE_PKG_NAME = "com.topjohnwu.unhide\0".getBytes();
public HideManager(Context context) {
super(context);
}
@Override
protected void onPreExecute() {
getMagiskManager().toast(R.string.hide_manager_toast, Toast.LENGTH_SHORT);
}
@Override
protected Boolean doInBackground(Void... voids) {
MagiskManager mm = getMagiskManager();
if (mm == null)
return false;
// Generate a new unhide app with random package name
File unhideAPK = new File(Environment.getExternalStorageDirectory() + "/MagiskManager", "unhide.apk");
unhideAPK.getParentFile().mkdirs();
String pkg;
try {
JarMap asset = new JarMap(mm.getAssets().open(UNHIDE_APK));
JarEntry je = new JarEntry(ANDROID_MANIFEST);
byte xml[] = asset.getRawData(je);
int offset = -1;
// Linear search pattern offset
for (int i = 0; i < xml.length - UNHIDE_PKG_NAME.length; ++i) {
boolean match = true;
for (int j = 0; j < UNHIDE_PKG_NAME.length; ++j) {
if (xml[i + j] != UNHIDE_PKG_NAME[j]) {
match = false;
break;
}
}
if (match) {
offset = i;
break;
}
}
if (offset < 0)
return false;
// Patch binary XML with new package name
pkg = Utils.genPackageName("com.", UNHIDE_PKG_NAME.length - 1);
System.arraycopy(pkg.getBytes(), 0, xml, offset, pkg.length());
asset.getOutputStream(je).write(xml);
// Sign the APK
ZipUtils.signZip(mm, asset, unhideAPK, false);
} catch (Exception e) {
e.printStackTrace();
return false;
}
// Install the application
List<String> ret = getShell().su("pm install " + unhideAPK + ">/dev/null && echo true || echo false");
unhideAPK.delete();
if (!Utils.isValidShellResponse(ret) || !Boolean.parseBoolean(ret.get(0)))
return false;
try {
// Allow the application to gain root by default
PackageManager pm = mm.getPackageManager();
int uid = pm.getApplicationInfo(pkg, 0).uid;
Policy policy = new Policy(uid, pm);
policy.policy = Policy.ALLOW;
policy.notification = false;
policy.logging = false;
mm.suDB.addPolicy(policy);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return false;
}
// Hide myself!
getShell().su_raw("pm hide " + mm.getPackageName());
return true;
}
@Override
protected void onPostExecute(Boolean b) {
MagiskManager mm = getMagiskManager();
if (mm == null)
return;
if (!b) {
mm.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
}
super.onPostExecute(b);
}
}

View File

@@ -0,0 +1,230 @@
package com.topjohnwu.magisk.asyncs;
import android.app.Activity;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.container.AdaptiveList;
import com.topjohnwu.magisk.container.TarEntry;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.ZipUtils;
import org.kamranzafar.jtar.TarInputStream;
import org.kamranzafar.jtar.TarOutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
public class InstallMagisk extends ParallelTask<Void, Void, Boolean> {
private static final int PATCH_MODE = 0;
private static final int DIRECT_MODE = 1;
private Uri mBootImg, mZip;
private AdaptiveList<String> mList;
private String mBootLocation;
private boolean mKeepEnc, mKeepVerity;
private int mode;
private InstallMagisk(Activity context, AdaptiveList<String> list, Uri zip, boolean enc, boolean verity) {
super(context);
mList = list;
mZip = zip;
mKeepEnc = enc;
mKeepVerity = verity;
}
public InstallMagisk(Activity context, AdaptiveList<String> list, Uri zip, boolean enc, boolean verity, Uri boot) {
this(context, list, zip, enc, verity);
mBootImg = boot;
mode = PATCH_MODE;
}
public InstallMagisk(Activity context, AdaptiveList<String> list, Uri zip, boolean enc, boolean verity, String boot) {
this(context, list, zip, enc, verity);
mBootLocation = boot;
mode = DIRECT_MODE;
}
@Override
protected void onPreExecute() {
// UI updates must run in the UI thread
mList.setCallback(this::publishProgress);
}
@Override
protected void onProgressUpdate(Void... values) {
mList.updateView();
}
@Override
protected Boolean doInBackground(Void... voids) {
MagiskManager mm = getMagiskManager();
if (mm == null) return false;
File install = new File(Utils.getEncContext(mm).getFilesDir().getParent(), "install");
getShell().sh_raw("rm -rf " + install);
List<String> abis = Arrays.asList(Build.SUPPORTED_ABIS);
String arch;
if (abis.contains("x86_64")) arch = "x64";
else if (abis.contains("arm64-v8a")) arch = "arm64";
else if (abis.contains("x86")) arch = "x86";
else arch = "arm";
mList.add("- Device platform: " + arch);
try {
// Unzip files
mList.add("- Extracting files");
try (InputStream in = mm.getContentResolver().openInputStream(mZip)) {
if (in == null) throw new FileNotFoundException();
BufferedInputStream buf = new BufferedInputStream(in);
buf.mark(Integer.MAX_VALUE);
ZipUtils.unzip(buf, install, arch + "/", true);
buf.reset();
ZipUtils.unzip(buf, install, "common/", true);
buf.reset();
ZipUtils.unzip(buf, install, "chromeos/", false);
buf.reset();
ZipUtils.unzip(buf, install, "META-INF/com/google/android/update-binary", true);
} catch (FileNotFoundException e) {
mList.add("! Invalid Uri");
throw e;
} catch (Exception e) {
mList.add("! Cannot unzip zip");
throw e;
}
File boot;
switch (mode) {
case PATCH_MODE:
boot = new File(install, "boot.img");
// Copy boot image to local
try (
InputStream in = mm.getContentResolver().openInputStream(mBootImg);
OutputStream out = new FileOutputStream(boot)
) {
InputStream source;
if (in == null) throw new FileNotFoundException();
if (Utils.getNameFromUri(mm, mBootImg).endsWith(".tar")) {
// Extract boot.img from tar
TarInputStream tar = new TarInputStream(new BufferedInputStream(in));
org.kamranzafar.jtar.TarEntry entry;
while ((entry = tar.getNextEntry()) != null) {
if (entry.getName().equals("boot.img"))
break;
}
source = tar;
} else {
// Direct copy raw image
source = new BufferedInputStream(in);
}
byte buffer[] = new byte[1024];
int length;
while ((length = source.read(buffer)) > 0)
out.write(buffer, 0, length);
} catch (FileNotFoundException e) {
mList.add("! Invalid Uri");
throw e;
} catch (IOException e) {
mList.add("! Copy failed");
throw e;
}
break;
case DIRECT_MODE:
boot = new File(mBootLocation);
break;
default:
return false;
}
mList.add("- Use boot image: " + boot);
Shell shell;
if (mode == PATCH_MODE && Shell.rootAccess()) {
// Force non-root shell
shell = Shell.getShell("sh");
} else {
shell = getShell();
}
// Patch boot image
shell.sh(mList,
"cd " + install,
"KEEPFORCEENCRYPT=" + mKeepEnc + " KEEPVERITY=" + mKeepVerity + " sh " +
"update-binary indep boot_patch.sh " + boot +
" && echo 'Success!' || echo 'Failed!'"
);
if (!TextUtils.equals(mList.get(mList.size() - 1), "Success!"))
return false;
File patched_boot = new File(install, "new-boot.img");
mList.add("");
switch (mode) {
case PATCH_MODE:
File dest = new File(Environment.getExternalStorageDirectory() + "/MagiskManager/patched_boot" + mm.bootFormat);
dest.getParentFile().mkdirs();
switch (mm.bootFormat) {
case ".img":
getShell().sh_raw("cp -f " + patched_boot + " " + dest);
break;
case ".img.tar":
TarOutputStream tar = new TarOutputStream(new BufferedOutputStream(new FileOutputStream(dest)));
tar.putNextEntry(new TarEntry(patched_boot, "boot.img"));
byte buffer[] = new byte[4096];
BufferedInputStream in = new BufferedInputStream(new FileInputStream(patched_boot));
int len;
while ((len = in.read(buffer)) != -1) {
tar.write(buffer, 0, len);
}
tar.flush();
tar.close();
in.close();
break;
}
mList.add("*********************************");
mList.add(" Patched Boot Image is placed in ");
mList.add(" " + dest + " ");
mList.add("*********************************");
break;
case DIRECT_MODE:
// Direct flash boot image
getShell().su(mList, "flash_boot_image " + patched_boot + " " + mBootLocation);
break;
default:
return false;
}
// Finals
getShell().sh_raw(
"cd " + install,
"mv bin/busybox busybox",
"rm -rf bin *.img update-binary",
"cd /");
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
super.onPostExecute(result);
}
}

View File

@@ -0,0 +1,37 @@
package com.topjohnwu.magisk.asyncs;
import android.content.Context;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.container.Module;
import com.topjohnwu.magisk.container.ValueSortedMap;
import com.topjohnwu.magisk.utils.Utils;
public class LoadModules extends ParallelTask<Void, Void, Void> {
public LoadModules(Context context) {
super(context);
}
@Override
protected Void doInBackground(Void... voids) {
MagiskManager mm = getMagiskManager();
if (mm == null) return null;
mm.moduleMap = new ValueSortedMap<>();
for (String path : Utils.getModList(getShell(), MagiskManager.MAGISK_PATH)) {
Module module = new Module(getShell(), path);
mm.moduleMap.put(module.getId(), module);
}
return null;
}
@Override
protected void onPostExecute(Void v) {
MagiskManager mm = getMagiskManager();
if (mm == null) return;
mm.moduleLoadDone.publish();
super.onPostExecute(v);
}
}

View File

@@ -0,0 +1,47 @@
package com.topjohnwu.magisk.asyncs;
import android.app.Activity;
import android.support.v7.app.AlertDialog;
import android.webkit.WebView;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.WebService;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
public class MarkDownWindow extends ParallelTask<Void, Void, String> {
private String mTitle, mUrl;
public MarkDownWindow(Activity context, String title, String url) {
super(context);
mTitle = title;
mUrl = url;
}
@Override
protected String doInBackground(Void... voids) {
String md = WebService.getString(mUrl);
Parser parser = Parser.builder().build();
HtmlRenderer renderer = HtmlRenderer.builder().build();
Node doc = parser.parse(md);
return String.format(
"<link rel='stylesheet' type='text/css' href='file:///android_asset/%s.css'/> %s",
getMagiskManager().isDarkTheme ? "dark" : "light", renderer.render(doc));
}
@Override
protected void onPostExecute(String html) {
AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
alert.setTitle(mTitle);
WebView wv = new WebView(getActivity());
wv.loadDataWithBaseURL("fake://", html, "text/html", "UTF-8", null);
alert.setView(wv);
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
alert.show();
}
}

View File

@@ -0,0 +1,59 @@
package com.topjohnwu.magisk.asyncs;
import android.app.Activity;
import android.content.Context;
import android.os.AsyncTask;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
import java.lang.ref.WeakReference;
public abstract class ParallelTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
private WeakReference<Activity> weakActivity;
private WeakReference<MagiskManager> weakMagiskManager;
private Runnable callback = null;
public ParallelTask() {}
public ParallelTask(Context context) {
weakMagiskManager = new WeakReference<>(Utils.getMagiskManager(context));
}
public ParallelTask(Activity context) {
this((Context) context);
weakActivity = new WeakReference<>(context);
}
protected Activity getActivity() {
return weakActivity.get();
}
protected MagiskManager getMagiskManager() {
return weakMagiskManager.get();
}
protected Shell getShell() {
MagiskManager magiskManager = getMagiskManager();
return magiskManager == null ? null : Shell.getShell(magiskManager);
}
@SuppressWarnings("unchecked")
public ParallelTask<Params, Progress, Result> exec(Params... params) {
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
return this;
}
@Override
protected void onPostExecute(Result result) {
if (callback != null) callback.run();
}
public ParallelTask<Params, Progress, Result> setCallBack(Runnable next) {
callback = next;
return this;
}
}

View File

@@ -0,0 +1,214 @@
package com.topjohnwu.magisk.asyncs;
import android.Manifest;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.widget.Toast;
import com.topjohnwu.magisk.FlashActivity;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.container.InputStreamWrapper;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.utils.WebService;
import com.topjohnwu.magisk.utils.ZipUtils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
public class ProcessRepoZip extends ParallelTask<Void, Object, Boolean> {
private ProgressDialog progressDialog;
private boolean mInstall;
private String mLink;
private File mFile;
private int progress = 0, total = -1;
private static final int UPDATE_DL_PROG = 0;
private static final int SHOW_PROCESSING = 1;
public ProcessRepoZip(Activity context, String link, String filename, boolean install) {
super(context);
mLink = link;
mFile = new File(Environment.getExternalStorageDirectory() + "/MagiskManager", filename);
mInstall = install;
}
private void removeTopFolder(InputStream in, File output) throws IOException {
JarInputStream source = new JarInputStream(in);
JarOutputStream dest = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(output)));
JarEntry entry;
String path;
int size;
byte buffer[] = new byte[4096];
while ((entry = source.getNextJarEntry()) != null) {
// Remove the top directory from the path
path = entry.getName().substring(entry.getName().indexOf("/") + 1);
// If it's the top folder, ignore it
if (path.isEmpty()) {
continue;
}
// Don't include placeholder
if (path.equals("system/placeholder")) {
continue;
}
dest.putNextEntry(new JarEntry(path));
while((size = source.read(buffer)) != -1) {
dest.write(buffer, 0, size);
}
}
source.close();
dest.close();
in.close();
}
@Override
protected void onPreExecute() {
Activity activity = getActivity();
mFile.getParentFile().mkdirs();
progressDialog = ProgressDialog.show(activity, activity.getString(R.string.zip_download_title), activity.getString(R.string.zip_download_msg, 0));
}
@Override
protected void onProgressUpdate(Object... values) {
int mode = (int) values[0];
switch (mode) {
case UPDATE_DL_PROG:
int add = (int) values[1];
progress += add;
progressDialog.setMessage(getActivity().getString(R.string.zip_download_msg, 100 * progress / total));
break;
case SHOW_PROCESSING:
progressDialog.setTitle(R.string.zip_process_title);
progressDialog.setMessage(getActivity().getString(R.string.zip_process_msg));
break;
}
}
@Override
protected Boolean doInBackground(Void... params) {
Activity activity = getActivity();
if (activity == null) return null;
try {
// Request zip from Internet
HttpURLConnection conn;
do {
conn = WebService.request(mLink, null);
if (conn == null) return null;
total = conn.getContentLength();
if (total < 0)
conn.disconnect();
else
break;
} while (true);
InputStream in = new BufferedInputStream(new ProgressInputStream(conn.getInputStream()));
// Temp files
File temp1 = new File(activity.getCacheDir(), "1.zip");
File temp2 = new File(temp1.getParentFile(), "2.zip");
temp1.getParentFile().mkdir();
// First remove top folder in Github source zip, Web -> temp1
removeTopFolder(in, temp1);
conn.disconnect();
publishProgress(SHOW_PROCESSING);
// Then sign the zip for the first time, temp1 -> temp2
ZipUtils.signZip(activity, temp1, temp2, false);
// Adjust the zip to prevent unzip issues, temp2 -> temp1
ZipUtils.zipAdjust(temp2.getPath(), temp1.getPath());
// Finally, sign the whole zip file again, temp1 -> temp2
ZipUtils.signZip(activity, temp1, temp2, true);
// Write it to the target zip, temp2 -> file
try (OutputStream out = new BufferedOutputStream(new FileOutputStream(mFile));
InputStream source = new BufferedInputStream(new FileInputStream(temp2))
) {
byte[] buffer = new byte[4096];
int length;
while ((length = source.read(buffer)) > 0)
out.write(buffer, 0, length);
}
// Delete temp files
temp1.delete();
temp2.delete();
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override
protected void onPostExecute(Boolean result) {
Activity activity = getActivity();
if (activity == null) return;
progressDialog.dismiss();
Uri uri = Uri.fromFile(mFile);
if (result) {
if (Shell.rootAccess() && mInstall) {
Intent intent = new Intent(getActivity(), FlashActivity.class);
intent.setData(uri).putExtra(FlashActivity.SET_ACTION, FlashActivity.FLASH_ZIP);
activity.startActivity(intent);
} else {
Utils.showUriSnack(activity, uri);
}
} else {
Utils.getMagiskManager(activity).toast(R.string.process_error, Toast.LENGTH_LONG);
}
super.onPostExecute(result);
}
@Override
public ParallelTask<Void, Object, Boolean> exec(Void... voids) {
Utils.runWithPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE,
() -> super.exec(voids));
return this;
}
private class ProgressInputStream extends InputStreamWrapper {
ProgressInputStream(InputStream in) {
super(in);
}
@Override
public synchronized int read() throws IOException {
publishProgress(UPDATE_DL_PROG, 1);
return super.read();
}
@Override
public int read(@NonNull byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public synchronized int read(@NonNull byte[] b, int off, int len) throws IOException {
int read = super.read(b, off, len);
publishProgress(UPDATE_DL_PROG, read);
return read;
}
}
}

View File

@@ -0,0 +1,40 @@
package com.topjohnwu.magisk.asyncs;
import android.content.Context;
import android.widget.Toast;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Utils;
import java.util.List;
public class RestoreStockBoot extends ParallelTask<Void, Void, Boolean> {
private String mBoot;
public RestoreStockBoot(Context context, String boot) {
super(context);
mBoot = boot;
}
@Override
protected Boolean doInBackground(Void... voids) {
List<String> ret = getShell().su("cat /init.magisk.rc | grep STOCKSHA1");
if (!Utils.isValidShellResponse(ret))
return false;
String stock_boot = "/data/stock_boot_" + ret.get(0).substring(ret.get(0).indexOf('=') + 1) + ".img.gz";
if (!Utils.itemExist(getShell(), stock_boot))
return false;
getShell().su_raw("flash_boot_image " + stock_boot + " " + mBoot);
return true;
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
getMagiskManager().toast(R.string.restore_done, Toast.LENGTH_SHORT);
} else {
getMagiskManager().toast(R.string.restore_fail, Toast.LENGTH_LONG);
}
}
}

View File

@@ -0,0 +1,219 @@
package com.topjohnwu.magisk.asyncs;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.ReposFragment;
import com.topjohnwu.magisk.container.Repo;
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.WebService;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.net.HttpURLConnection;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class UpdateRepos extends ParallelTask<Void, Void, Void> {
private static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&page=%d";
public static final String ETAG_KEY = "ETag";
private static final String IF_NONE_MATCH = "If-None-Match";
private static final String LINK_KEY = "Link";
private static final int CHECK_ETAG = 0;
private static final int LOAD_NEXT = 1;
private static final int LOAD_PREV = 2;
private List<String> cached, etags, newEtags = new ArrayList<>();
private RepoDatabaseHelper repoDB;
private SharedPreferences prefs;
private boolean forceUpdate;
private int tasks = 0;
public UpdateRepos(Context context, boolean force) {
super(context);
prefs = getMagiskManager().prefs;
repoDB = getMagiskManager().repoDB;
getMagiskManager().repoLoadDone.hasPublished = false;
// Legacy data cleanup
File old = new File(context.getApplicationInfo().dataDir + "/shared_prefs", "RepoMap.xml");
if (old.exists() || prefs.getString("repomap", null) != null) {
old.delete();
prefs.edit().remove("version").remove("repomap").remove(ETAG_KEY).apply();
repoDB.clearRepo();
}
forceUpdate = force;
}
private void loadJSON(String jsonString) throws Exception {
JSONArray jsonArray = new JSONArray(jsonString);
// Empty page, throw error
if (jsonArray.length() == 0) throw new Exception();
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonobject = jsonArray.getJSONObject(i);
String id = jsonobject.getString("description");
String name = jsonobject.getString("name");
String lastUpdate = jsonobject.getString("pushed_at");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
Date updatedDate = format.parse(lastUpdate);
++tasks;
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
Repo repo = repoDB.getRepo(id);
Boolean updated;
try {
if (repo == null) {
repo = new Repo(name, updatedDate);
updated = true;
} else {
// Popout from cached
cached.remove(id);
if (forceUpdate) {
repo.update();
updated = true;
} else {
updated = repo.update(updatedDate);
}
}
if (updated) {
repoDB.addRepo(repo);
publishProgress();
}
if (!id.equals(repo.getId())) {
Logger.error("Repo [" + name + "] id=[" + repo.getId() + "] has illegal repo id");
}
} catch (Repo.IllegalRepoException e) {
Logger.error(e.getMessage());
repoDB.removeRepo(id);
}
--tasks;
});
}
}
private boolean loadPage(int page, int mode) {
Map<String, String> header = new HashMap<>();
String etag = "";
if (mode == CHECK_ETAG && page < etags.size()) {
etag = etags.get(page);
}
header.put(IF_NONE_MATCH, etag);
String url = String.format(Locale.US, REPO_URL, page + 1);
HttpURLConnection conn = WebService.request(url, header);
try {
if (conn == null) throw new Exception();
if (conn.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) {
newEtags.add(etag);
return page + 1 < etags.size() && loadPage(page + 1, CHECK_ETAG);
}
loadJSON(WebService.getString(conn));
} catch (Exception e) {
e.printStackTrace();
// Don't continue
return true;
}
// Update ETAG
etag = header.get(ETAG_KEY);
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
newEtags.add(etag);
String links = header.get(LINK_KEY);
if (links != null) {
for (String s : links.split(", ")) {
if (mode != LOAD_PREV && s.contains("next")) {
// Force load all next pages
loadPage(page + 1, LOAD_NEXT);
} else if (mode != LOAD_NEXT && s.contains("prev")) {
// Back propagation
loadPage(page - 1, LOAD_PREV);
}
}
}
return true;
}
private Void waitTasks() {
while (tasks > 0) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
@Override
protected void onProgressUpdate(Void... values) {
if (ReposFragment.adapter != null)
ReposFragment.adapter.notifyDBChanged();
}
@Override
protected Void doInBackground(Void... voids) {
etags = new ArrayList<>(Arrays.asList(prefs.getString(ETAG_KEY, "").split(",")));
cached = repoDB.getRepoIDList();
if (!loadPage(0, CHECK_ETAG)) {
// Nothing changed online
if (forceUpdate) {
for (String id : cached) {
if (id == null) continue;
++tasks;
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
Repo repo = repoDB.getRepo(id);
try {
repo.update();
repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.error(e.getMessage());
repoDB.removeRepo(repo);
}
--tasks;
});
}
}
return waitTasks();
}
// Wait till all tasks are done
waitTasks();
// The leftover cached means they are removed from online repo
repoDB.removeRepo(cached);
// Update ETag
StringBuilder etagBuilder = new StringBuilder();
for (int i = 0; i < newEtags.size(); ++i) {
if (i != 0) etagBuilder.append(",");
etagBuilder.append(newEtags.get(i));
}
prefs.edit().putString(ETAG_KEY, etagBuilder.toString()).apply();
return null;
}
@Override
protected void onPostExecute(Void v) {
MagiskManager mm = getMagiskManager();
if (mm == null) return;
mm.repoLoadDone.publish();
super.onPostExecute(v);
}
}

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.topjohnwu.magisk;
package com.topjohnwu.magisk.components;
import android.content.Context;
import android.content.res.TypedArray;
@@ -26,6 +26,8 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
/**
* @author dvdandroid
*/

View File

@@ -0,0 +1,79 @@
package com.topjohnwu.magisk.components;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
public class Activity extends AppCompatActivity {
private Runnable permissionGrantCallback;
public Activity() {
super();
Configuration configuration = new Configuration();
configuration.setLocale(MagiskManager.locale);
applyOverrideConfiguration(configuration);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (this instanceof Topic.Subscriber) {
((Topic.Subscriber) this).subscribeTopics();
}
}
@Override
protected void onDestroy() {
if (this instanceof Topic.Subscriber) {
((Topic.Subscriber) this).unsubscribeTopics();
}
super.onDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (permissionGrantCallback != null) {
permissionGrantCallback.run();
}
}
permissionGrantCallback = null;
}
public void setPermissionGrantCallback(Runnable callback) {
permissionGrantCallback = callback;
}
public MagiskManager getMagiskManager() {
return (MagiskManager) super.getApplicationContext();
}
public Shell getShell() {
return Shell.getShell(this);
}
protected void setFloating() {
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
if (isTablet) {
WindowManager.LayoutParams params = getWindow().getAttributes();
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
params.alpha = 1.0f;
params.dimAmount = 0.6f;
params.flags |= 2;
getWindow().setAttributes(params);
setFinishOnTouchOutside(true);
}
}
}

View File

@@ -0,0 +1,161 @@
package com.topjohnwu.magisk.components;
import android.app.Activity;
import android.content.DialogInterface;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.StyleRes;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.topjohnwu.magisk.R;
import butterknife.BindView;
import butterknife.ButterKnife;
public class AlertDialogBuilder extends AlertDialog.Builder {
@BindView(R.id.button_panel) LinearLayout buttons;
@BindView(R.id.message_panel) LinearLayout messagePanel;
@BindView(R.id.negative) Button negative;
@BindView(R.id.positive) Button positive;
@BindView(R.id.neutral) Button neutral;
@BindView(R.id.message) TextView messageView;
@BindView(R.id.custom_view) ViewStub custom;
private DialogInterface.OnClickListener positiveListener;
private DialogInterface.OnClickListener negativeListener;
private DialogInterface.OnClickListener neutralListener;
private AlertDialog dialog;
public AlertDialogBuilder(@NonNull Activity context) {
super(context);
setup();
}
public AlertDialogBuilder(@NonNull Activity context, @StyleRes int themeResId) {
super(context, themeResId);
setup();
}
private void setup() {
View v = LayoutInflater.from(getContext()).inflate(R.layout.alert_dialog, null);
ButterKnife.bind(this, v);
super.setView(v);
negative.setVisibility(View.GONE);
positive.setVisibility(View.GONE);
neutral.setVisibility(View.GONE);
buttons.setVisibility(View.GONE);
messagePanel.setVisibility(View.GONE);
}
@Override
public AlertDialog.Builder setView(int layoutResId) {
custom.setLayoutResource(layoutResId);
custom.inflate();
return this;
}
@Override
public AlertDialog.Builder setView(View view) {
ViewGroup parent = (ViewGroup) custom.getParent();
int idx = parent.indexOfChild(custom);
parent.removeView(custom);
parent.addView(view, idx);
return this;
}
@Override
public AlertDialog.Builder setMessage(@Nullable CharSequence message) {
messageView.setText(message);
messagePanel.setVisibility(View.VISIBLE);
return this;
}
@Override
public AlertDialog.Builder setMessage(@StringRes int messageId) {
return setMessage(getContext().getString(messageId));
}
@Override
public AlertDialog.Builder setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) {
buttons.setVisibility(View.VISIBLE);
positive.setVisibility(View.VISIBLE);
positive.setText(text);
positiveListener = listener;
positive.setOnClickListener((v) -> {
if (positiveListener != null) {
positiveListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
}
dialog.dismiss();
});
return this;
}
@Override
public AlertDialog.Builder setPositiveButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
return setPositiveButton(getContext().getString(textId), listener);
}
@Override
public AlertDialog.Builder setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) {
buttons.setVisibility(View.VISIBLE);
negative.setVisibility(View.VISIBLE);
negative.setText(text);
negativeListener = listener;
negative.setOnClickListener((v) -> {
if (negativeListener != null) {
negativeListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
}
dialog.dismiss();
});
return this;
}
@Override
public AlertDialog.Builder setNegativeButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
return setNegativeButton(getContext().getString(textId), listener);
}
@Override
public AlertDialog.Builder setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) {
buttons.setVisibility(View.VISIBLE);
neutral.setVisibility(View.VISIBLE);
neutral.setText(text);
neutralListener = listener;
neutral.setOnClickListener((v) -> {
if (neutralListener != null) {
neutralListener.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
}
dialog.dismiss();
});
return this;
}
@Override
public AlertDialog.Builder setNeutralButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
return setNeutralButton(getContext().getString(textId), listener);
}
@Override
public AlertDialog create() {
dialog = super.create();
return dialog;
}
@Override
public AlertDialog show() {
create();
dialog.show();
return dialog;
}
}

View File

@@ -0,0 +1,84 @@
package com.topjohnwu.magisk.components;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
public interface ExpandableView {
class Container {
public ViewGroup expandLayout;
ValueAnimator expandAnimator, collapseAnimator;
boolean mExpanded = false;
int expandHeight = 0;
}
// Provide state info
Container getContainer();
default void setupExpandable() {
Container container = getContainer();
container.expandLayout.getViewTreeObserver().addOnPreDrawListener(
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
if (container.expandHeight == 0) {
final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
container.expandLayout.measure(widthSpec, heightSpec);
container.expandHeight = container.expandLayout.getMeasuredHeight();
}
container.expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
container.expandLayout.setVisibility(View.GONE);
container.expandAnimator = slideAnimator(0, container.expandHeight);
container.collapseAnimator = slideAnimator(container.expandHeight, 0);
return true;
}
});
}
default boolean isExpanded() {
return getContainer().mExpanded;
}
default void setExpanded(boolean expanded) {
Container container = getContainer();
container.mExpanded = expanded;
ViewGroup.LayoutParams layoutParams = container.expandLayout.getLayoutParams();
layoutParams.height = expanded ? container.expandHeight : 0;
container.expandLayout.setLayoutParams(layoutParams);
container.expandLayout.setVisibility(expanded ? View.VISIBLE : View.GONE);
}
default void expand() {
Container container = getContainer();
if (container.mExpanded) return;
container.expandLayout.setVisibility(View.VISIBLE);
container.expandAnimator.start();
container.mExpanded = true;
}
default void collapse() {
Container container = getContainer();
if (!container.mExpanded) return;
container.collapseAnimator.start();
container.mExpanded = false;
}
default ValueAnimator slideAnimator(int start, int end) {
Container container = getContainer();
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.addUpdateListener(valueAnimator -> {
int value = (Integer) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams layoutParams = container.expandLayout.getLayoutParams();
layoutParams.height = value;
container.expandLayout.setLayoutParams(layoutParams);
});
return animator;
}
}

View File

@@ -0,0 +1,53 @@
package com.topjohnwu.magisk.components;
import android.content.Intent;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Topic;
import com.topjohnwu.magisk.utils.Utils;
public class Fragment extends android.support.v4.app.Fragment {
private ActivityResultListener activityResultListener;
public MagiskManager getApplication() {
return Utils.getMagiskManager(getActivity());
}
public Shell getShell() {
return Shell.getShell(getActivity());
}
@Override
public void onResume() {
super.onResume();
if (this instanceof Topic.Subscriber) {
((Topic.Subscriber) this).subscribeTopics();
}
}
@Override
public void onPause() {
if (this instanceof Topic.Subscriber) {
((Topic.Subscriber) this).unsubscribeTopics();
}
super.onPause();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (activityResultListener != null)
activityResultListener.onActivityResult(requestCode, resultCode, data);
activityResultListener = null;
}
public void startActivityForResult(Intent intent, int requestCode, ActivityResultListener listener) {
activityResultListener = listener;
super.startActivityForResult(intent, requestCode);
}
public interface ActivityResultListener {
void onActivityResult(int requestCode, int resultCode, Intent data);
}
}

View File

@@ -0,0 +1,37 @@
package com.topjohnwu.magisk.components;
import android.app.Activity;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.view.View;
import android.widget.TextView;
public class SnackbarMaker {
public static Snackbar make(Activity activity, CharSequence text, int duration) {
View view = activity.findViewById(android.R.id.content);
return make(view, text, duration);
}
public static Snackbar make(Activity activity, @StringRes int resId, int duration) {
return make(activity, activity.getString(resId), duration);
}
public static Snackbar make(View view, CharSequence text, int duration) {
Snackbar snack = Snackbar.make(view, text, duration);
setup(snack);
return snack;
}
public static Snackbar make(View view, @StringRes int resId, int duration) {
Snackbar snack = Snackbar.make(view, resId, duration);
setup(snack);
return snack;
}
private static void setup(Snackbar snack) {
TextView text = snack.getView().findViewById(android.support.design.R.id.snackbar_text);
text.setMaxLines(Integer.MAX_VALUE);
}
}

View File

@@ -0,0 +1,36 @@
package com.topjohnwu.magisk.container;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
public class AdaptiveList<E> extends ArrayList<E> {
private Runnable callback;
private RecyclerView mView;
public AdaptiveList(RecyclerView v) {
mView = v;
}
public void updateView() {
mView.getAdapter().notifyDataSetChanged();
mView.scrollToPosition(mView.getAdapter().getItemCount() - 1);
}
public void setCallback(Runnable cb) {
callback = cb;
}
public boolean add(E e) {
boolean ret = super.add(e);
if (ret) {
if (callback == null) {
updateView();
} else {
callback.run();
}
}
return ret;
}
}

View File

@@ -0,0 +1,119 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.database.Cursor;
import android.support.annotation.NonNull;
import java.util.List;
public abstract class BaseModule implements Comparable<BaseModule> {
private String mId = null, mName, mVersion, mAuthor, mDescription;
private int mVersionCode = -1, templateVersion = -1;
protected BaseModule() {}
protected BaseModule(Cursor c) {
mId = c.getString(c.getColumnIndex("id"));
mName = c.getString(c.getColumnIndex("name"));
mVersion = c.getString(c.getColumnIndex("version"));
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
mAuthor = c.getString(c.getColumnIndex("author"));
mDescription = c.getString(c.getColumnIndex("description"));
templateVersion = c.getInt(c.getColumnIndex("template"));
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("id", mId);
values.put("name", mName);
values.put("version", mVersion);
values.put("versionCode", mVersionCode);
values.put("author", mAuthor);
values.put("description", mDescription);
values.put("template", templateVersion);
return values;
}
protected void parseProps(List<String> props) { parseProps(props.toArray(new String[0])); }
protected void parseProps(String[] props) throws NumberFormatException {
for (String line : props) {
String[] prop = line.split("=", 2);
if (prop.length != 2)
continue;
String key = prop[0].trim();
if (key.charAt(0) == '#')
continue;
switch (key) {
case "id":
mId = prop[1];
break;
case "name":
mName = prop[1];
break;
case "version":
mVersion = prop[1];
break;
case "versionCode":
mVersionCode = Integer.parseInt(prop[1]);
break;
case "author":
mAuthor = prop[1];
break;
case "description":
mDescription = prop[1];
break;
case "template":
templateVersion = Integer.parseInt(prop[1]);
break;
default:
break;
}
}
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public String getVersion() {
return mVersion;
}
public String getAuthor() {
return mAuthor;
}
public String getId() {
return mId;
}
public void setId(String id) {
mId = id;
}
public String getDescription() {
return mDescription;
}
public int getVersionCode() {
return mVersionCode;
}
public int getTemplateVersion() {
return templateVersion;
}
@Override
public int compareTo(@NonNull BaseModule module) {
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
}
}

View File

@@ -0,0 +1,59 @@
package com.topjohnwu.magisk.container;
import android.support.annotation.NonNull;
import java.io.IOException;
import java.io.InputStream;
public class InputStreamWrapper extends InputStream {
private InputStream in;
public InputStreamWrapper(InputStream in) {
this.in = in;
}
@Override
public int available() throws IOException {
return in.available();
}
@Override
public void close() throws IOException {
in.close();
}
@Override
public synchronized void mark(int readlimit) {
in.mark(readlimit);
}
@Override
public boolean markSupported() {
return in.markSupported();
}
@Override
public synchronized int read() throws IOException {
return in.read();
}
@Override
public int read(@NonNull byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public synchronized int read(@NonNull byte[] b, int off, int len) throws IOException {
return in.read(b, off, len);
}
@Override
public synchronized void reset() throws IOException {
in.reset();
}
@Override
public long skip(long n) throws IOException {
return in.skip(n);
}
}

View File

@@ -0,0 +1,67 @@
package com.topjohnwu.magisk.container;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.Utils;
public class Module extends BaseModule {
private String mRemoveFile, mDisableFile, mUpdateFile;
private boolean mEnable, mRemove, mUpdated;
public Module(Shell shell, String path) {
try {
parseProps(Utils.readFile(shell, path + "/module.prop"));
} catch (NumberFormatException ignored) {}
mRemoveFile = path + "/remove";
mDisableFile = path + "/disable";
mUpdateFile = path + "/update";
if (getId() == null) {
int sep = path.lastIndexOf('/');
setId(path.substring(sep + 1));
}
if (getName() == null) {
setName(getId());
}
mEnable = !Utils.itemExist(shell, mDisableFile);
mRemove = Utils.itemExist(shell, mRemoveFile);
mUpdated = Utils.itemExist(shell, mUpdateFile);
}
public void createDisableFile(Shell shell) {
mEnable = false;
Utils.createFile(shell, mDisableFile);
}
public void removeDisableFile(Shell shell) {
mEnable = true;
Utils.removeItem(shell, mDisableFile);
}
public boolean isEnabled() {
return mEnable;
}
public void createRemoveFile(Shell shell) {
mRemove = true;
Utils.createFile(shell, mRemoveFile);
}
public void deleteRemoveFile(Shell shell) {
mRemove = false;
Utils.removeItem(shell, mRemoveFile);
}
public boolean willBeRemoved() {
return mRemove;
}
public boolean isUpdated() {
return mUpdated;
}
}

View File

@@ -0,0 +1,57 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.support.annotation.NonNull;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;
public static final int DENY = 1;
public static final int ALLOW = 2;
public int uid, policy = INTERACTIVE;
public long until;
public boolean logging = true, notification = true;
public String packageName, appName;
public ApplicationInfo info;
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
String[] pkgs = pm.getPackagesForUid(uid);
if (pkgs != null && pkgs.length > 0) {
this.uid = uid;
packageName = pkgs[0];
info = pm.getApplicationInfo(packageName, 0);
appName = info.loadLabel(pm).toString();
} else throw new PackageManager.NameNotFoundException();
}
public Policy(Cursor c, PackageManager pm) throws PackageManager.NameNotFoundException {
uid = c.getInt(c.getColumnIndex("uid"));
packageName = c.getString(c.getColumnIndex("package_name"));
policy = c.getInt(c.getColumnIndex("policy"));
until = c.getLong(c.getColumnIndex("until"));
logging = c.getInt(c.getColumnIndex("logging")) != 0;
notification = c.getInt(c.getColumnIndex("notification")) != 0;
info = pm.getApplicationInfo(packageName, 0);
appName = info.loadLabel(pm).toString();
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("uid", uid);
values.put("package_name", packageName);
values.put("policy", policy);
values.put("until", until);
values.put("logging", logging ? 1 : 0);
values.put("notification", notification ? 1 : 0);
return values;
}
@Override
public int compareTo(@NonNull Policy policy) {
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
}
}

View File

@@ -0,0 +1,93 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.database.Cursor;
import com.topjohnwu.magisk.utils.WebService;
import java.util.Date;
public class Repo extends BaseModule {
public static final int MIN_TEMPLATE_VER = 4;
private static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
private static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
private String repoName;
private Date mLastUpdate;
public Repo(String name, Date lastUpdate) throws IllegalRepoException {
mLastUpdate = lastUpdate;
repoName = name;
update();
}
public Repo(Cursor c) {
super(c);
repoName = c.getString(c.getColumnIndex("repo_name"));
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
}
public void update() throws IllegalRepoException {
String props = WebService.getString(getManifestUrl());
String lines[] = props.split("\\n");
try {
parseProps(lines);
} catch (NumberFormatException e) {
throw new IllegalRepoException("Repo [" + repoName + "] parse error: " + e.getMessage());
}
if (getId() == null) {
throw new IllegalRepoException("Repo [" + repoName + "] does not contain id");
}
if (getVersionCode() < 0) {
throw new IllegalRepoException("Repo [" + repoName + "] does not contain versionCode");
}
if (getTemplateVersion() < MIN_TEMPLATE_VER) {
throw new IllegalRepoException("Repo [" + repoName + "] is outdated");
}
}
public boolean update(Date lastUpdate) throws IllegalRepoException {
if (lastUpdate.after(mLastUpdate)) {
mLastUpdate = lastUpdate;
update();
return true;
}
return false;
}
public ContentValues getContentValues() {
ContentValues values = super.getContentValues();
values.put("repo_name", repoName);
values.put("last_update", mLastUpdate.getTime());
return values;
}
public String getRepoName() {
return repoName;
}
public String getZipUrl() {
return String.format(ZIP_URL, repoName);
}
public String getManifestUrl() {
return String.format(FILE_URL, repoName, "module.prop");
}
public String getDetailUrl() {
return String.format(FILE_URL, repoName, "README.md");
}
public Date getLastUpdate() {
return mLastUpdate;
}
public class IllegalRepoException extends Exception {
IllegalRepoException(String message) {
super(message);
}
}
}

View File

@@ -0,0 +1,56 @@
package com.topjohnwu.magisk.container;
import android.content.ContentValues;
import android.database.Cursor;
import com.topjohnwu.magisk.MagiskManager;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SuLogEntry {
public int fromUid, toUid, fromPid;
public String packageName, appName, command;
public boolean action;
public Date date;
public SuLogEntry(Policy policy) {
fromUid = policy.uid;
packageName = policy.packageName;
appName = policy.appName;
}
public SuLogEntry(Cursor c) {
fromUid = c.getInt(c.getColumnIndex("from_uid"));
fromPid = c.getInt(c.getColumnIndex("from_pid"));
toUid = c.getInt(c.getColumnIndex("to_uid"));
packageName = c.getString(c.getColumnIndex("package_name"));
appName = c.getString(c.getColumnIndex("app_name"));
command = c.getString(c.getColumnIndex("command"));
action = c.getInt(c.getColumnIndex("action")) != 0;
date = new Date(c.getLong(c.getColumnIndex("time")));
}
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
values.put("from_uid", fromUid);
values.put("package_name", packageName);
values.put("app_name", appName);
values.put("from_pid", fromPid);
values.put("command", command);
values.put("to_uid", toUid);
values.put("action", action ? 1 : 0);
values.put("time", date.getTime());
return values;
}
public String getDateString() {
return DateFormat.getDateInstance(DateFormat.MEDIUM, MagiskManager.locale).format(date);
}
public String getTimeString() {
return new SimpleDateFormat("h:mm a", MagiskManager.locale).format(date);
}
}

View File

@@ -0,0 +1,64 @@
package com.topjohnwu.magisk.container;
import org.kamranzafar.jtar.TarHeader;
import java.io.File;
import java.util.Arrays;
public class TarEntry extends org.kamranzafar.jtar.TarEntry {
public TarEntry(File file, String entryName) {
super(file, entryName);
}
/*
* Workaround missing java.nio.file.attribute.PosixFilePermission
* Simply just assign a default permission to the file
* */
@Override
public void extractTarHeader(String entryName) {
int permissions = file.isDirectory() ? 000755 : 000644;
header = TarHeader.createHeader(entryName, file.length(), file.lastModified() / 1000, file.isDirectory(), permissions);
header.userName = new StringBuffer("");
header.groupName = header.userName;
}
/*
* Rewrite the header to GNU format
* */
@Override
public void writeEntryHeader(byte[] outbuf) {
super.writeEntryHeader(outbuf);
System.arraycopy("ustar \0".getBytes(), 0, outbuf, 257, TarHeader.USTAR_MAGICLEN);
getOctalBytes(header.mode, outbuf, 100, TarHeader.MODELEN);
getOctalBytes(header.userId, outbuf, 108, TarHeader.UIDLEN);
getOctalBytes(header.groupId, outbuf, 116, TarHeader.GIDLEN);
getOctalBytes(header.size, outbuf, 124, TarHeader.SIZELEN);
getOctalBytes(header.modTime, outbuf, 136, TarHeader.MODTIMELEN);
Arrays.fill(outbuf, 148, 148 + TarHeader.CHKSUMLEN, (byte) ' ');
Arrays.fill(outbuf, 329, 329 + TarHeader.USTAR_DEVLEN, (byte) '\0');
Arrays.fill(outbuf, 337, 337 + TarHeader.USTAR_DEVLEN, (byte) '\0');
// Recalculate checksum
getOctalBytes(computeCheckSum(outbuf), outbuf, 148, TarHeader.CHKSUMLEN);
}
/*
* Proper octal to ASCII conversion
* */
private void getOctalBytes(long value, byte[] buf, int offset, int length) {
int idx = length - 1;
buf[offset + idx] = 0;
--idx;
for (long val = value; idx >= 0; --idx) {
buf[offset + idx] = (byte) ((byte) '0' + (byte) (val & 7));
val = val >> 3;
}
}
}

View File

@@ -0,0 +1,43 @@
package com.topjohnwu.magisk.container;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ValueSortedMap<K, V extends Comparable<? super V>> extends HashMap<K, V> {
private List<V> sorted = new ArrayList<>();
@NonNull
@Override
public Collection<V> values() {
if (sorted.isEmpty()) {
sorted.addAll(super.values());
Collections.sort(sorted);
}
return sorted;
}
@Override
public V put(K key, V value) {
sorted.clear();
return super.put(key, value);
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
sorted.clear();
super.putAll(m);
}
@Override
public V remove(Object key) {
sorted.clear();
return super.remove(key);
}
}

View File

@@ -0,0 +1,102 @@
package com.topjohnwu.magisk.database;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.container.Repo;
import com.topjohnwu.magisk.utils.Utils;
import java.util.LinkedList;
import java.util.List;
public class RepoDatabaseHelper extends SQLiteOpenHelper {
private static final int DATABASE_VER = 2;
private static final String TABLE_NAME = "repos";
private SQLiteDatabase mDb;
private MagiskManager mm;
public RepoDatabaseHelper(Context context) {
super(context, "repo.db", null, DATABASE_VER);
mDb = getWritableDatabase();
mm = Utils.getMagiskManager(context);
// Clear bad repos
mDb.delete(TABLE_NAME, "template<?",
new String[] { String.valueOf(Repo.MIN_TEMPLATE_VER) });
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, DATABASE_VER);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 0) {
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
"author TEXT, description TEXT, repo_name TEXT, last_update INT, " +
"PRIMARY KEY(id))");
oldVersion++;
}
if (oldVersion == 1) {
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD template INT");
oldVersion++;
}
}
public void clearRepo() {
mDb.delete(TABLE_NAME, null, null);
}
public void removeRepo(String id) {
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
}
public void removeRepo(Repo repo) {
mDb.delete(TABLE_NAME, "repo_name=?", new String[] { repo.getRepoName() });
}
public void removeRepo(List<String> list) {
for (String id : list) {
if (id == null) continue;
mDb.delete(TABLE_NAME, "id=?", new String[] { id });
}
}
public void addRepo(Repo repo) {
mDb.replace(TABLE_NAME, null, repo.getContentValues());
}
public Repo getRepo(String id) {
try (Cursor c = mDb.query(TABLE_NAME, null, "id=?", new String[] { id }, null, null, null)) {
if (c.moveToNext()) {
return new Repo(c);
}
}
return null;
}
public Cursor getRepoCursor() {
return mDb.query(TABLE_NAME, null, "template<=?",
new String[] { String.valueOf(mm.magiskVersionCode) },
null, null, "name COLLATE NOCASE");
}
public List<String> getRepoIDList() {
LinkedList<String> ret = new LinkedList<>();
try (Cursor c = mDb.query(TABLE_NAME, null, null, null, null, null, null)) {
while (c.moveToNext()) {
ret.add(c.getString(c.getColumnIndex("id")));
}
}
return ret;
}
}

View File

@@ -0,0 +1,265 @@
package com.topjohnwu.magisk.database;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.text.TextUtils;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.container.SuLogEntry;
import com.topjohnwu.magisk.utils.Utils;
import java.io.File;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
public class SuDatabaseHelper extends SQLiteOpenHelper {
public static final String ROOT_ACCESS = "root_access";
public static final int ROOT_ACCESS_DISABLED = 0;
public static final int ROOT_ACCESS_APPS_ONLY = 1;
public static final int ROOT_ACCESS_ADB_ONLY = 2;
public static final int ROOT_ACCESS_APPS_AND_ADB = 3;
public static final String MULTIUSER_MODE = "multiuser_mode";
public static final int MULTIUSER_MODE_OWNER_ONLY = 0;
public static final int MULTIUSER_MODE_OWNER_MANAGED = 1;
public static final int MULTIUSER_MODE_USER = 2;
public static final String MNT_NS = "mnt_ns";
public static final int NAMESPACE_MODE_GLOBAL = 0;
public static final int NAMESPACE_MODE_REQUESTER = 1;
public static final int NAMESPACE_MODE_ISOLATE = 2;
public static final String DB_NAME = "su.db";
private static final int DATABASE_VER = 3;
private static final String POLICY_TABLE = "policies";
private static final String LOG_TABLE = "logs";
private static final String SETTINGS_TABLE = "settings";
private MagiskManager mm;
private PackageManager pm;
private SQLiteDatabase mDb;
public SuDatabaseHelper(Context context) {
super(context, DB_NAME, null, DATABASE_VER);
mm = Utils.getMagiskManager(context);
pm = context.getPackageManager();
mDb = getWritableDatabase();
cleanup();
}
@Override
public void onCreate(SQLiteDatabase db) {
onUpgrade(db, 0, DATABASE_VER);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion == 0) {
createTables(db);
oldVersion = 2;
}
if (oldVersion == 1) {
// We're dropping column app_name, rename and re-construct table
db.execSQL("ALTER TABLE " + POLICY_TABLE + " RENAME TO " + POLICY_TABLE + "_old");
// Create the new tables
createTables(db);
// Migrate old data to new tables
db.execSQL(
"INSERT INTO " + POLICY_TABLE + " SELECT " +
"uid, package_name, policy, until, logging, notification " +
"FROM " + POLICY_TABLE + "_old");
db.execSQL("DROP TABLE " + POLICY_TABLE + "_old");
File oldDB = mm.getDatabasePath("sulog.db");
if (oldDB.exists()) {
migrateLegacyLogList(oldDB, db);
mm.deleteDatabase("sulog.db");
}
++oldVersion;
}
if (oldVersion == 2) {
db.execSQL("UPDATE " + LOG_TABLE + " SET time=time*1000");
++oldVersion;
}
}
private void createTables(SQLiteDatabase db) {
// Policies
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + POLICY_TABLE + " " +
"(uid INT, package_name TEXT, policy INT, " +
"until INT, logging INT, notification INT, " +
"PRIMARY KEY(uid))");
// Logs
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + LOG_TABLE + " " +
"(from_uid INT, package_name TEXT, app_name TEXT, from_pid INT, " +
"to_uid INT, action INT, time INT, command TEXT)");
// Settings
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + SETTINGS_TABLE + " " +
"(key TEXT, value INT, PRIMARY KEY(key))");
}
private void cleanup() {
// Clear outdated policies
mDb.delete(POLICY_TABLE, "until > 0 AND until < ?",
new String[] { String.valueOf(System.currentTimeMillis() / 1000) });
// Clear outdated logs
mDb.delete(LOG_TABLE, "time < ?", new String[] { String.valueOf(
System.currentTimeMillis() - mm.suLogTimeout * 86400000) });
}
public void deletePolicy(Policy policy) {
deletePolicy(policy.packageName);
}
public void deletePolicy(String pkg) {
mDb.delete(POLICY_TABLE, "package_name=?", new String[] { pkg });
}
public void deletePolicy(int uid) {
mDb.delete(POLICY_TABLE, "uid=?", new String[]{String.valueOf(uid)});
}
public Policy getPolicy(int uid) {
Policy policy = null;
try (Cursor c = mDb.query(POLICY_TABLE, null, "uid=?", new String[] { String.valueOf(uid) }, null, null, null)) {
if (c.moveToNext()) {
policy = new Policy(c, pm);
}
} catch (PackageManager.NameNotFoundException e) {
deletePolicy(uid);
return null;
}
return policy;
}
public Policy getPolicy(String pkg) {
Policy policy = null;
try (Cursor c = mDb.query(POLICY_TABLE, null, "package_name=?", new String[] { pkg }, null, null, null)) {
if (c.moveToNext()) {
policy = new Policy(c, pm);
}
} catch (PackageManager.NameNotFoundException e) {
deletePolicy(pkg);
return null;
}
return policy;
}
public void addPolicy(Policy policy) {
mDb.replace(POLICY_TABLE, null, policy.getContentValues());
}
public void updatePolicy(Policy policy) {
mDb.update(POLICY_TABLE, policy.getContentValues(), "package_name=?",
new String[] { policy.packageName });
}
public List<Policy> getPolicyList(PackageManager pm) {
try (Cursor c = mDb.query(POLICY_TABLE, null, null, null, null, null, null)) {
List<Policy> ret = new ArrayList<>(c.getCount());
while (c.moveToNext()) {
try {
Policy policy = new Policy(c, pm);
// The application changed UID for some reason, check user config
if (policy.info.uid != policy.uid) {
if (mm.suReauth) {
// Reauth required, remove from DB
deletePolicy(policy);
continue;
} else {
// No reauth, update to use the new UID
policy.uid = policy.info.uid;
updatePolicy(policy);
}
}
ret.add(policy);
} catch (PackageManager.NameNotFoundException e) {
// The app no longer exist, remove from DB
deletePolicy(c.getInt(c.getColumnIndex("uid")));
}
}
Collections.sort(ret);
return ret;
}
}
public List<List<Integer>> getLogStructure() {
try (Cursor c = mDb.query(LOG_TABLE, new String[] { "time" }, null, null, null, null, "time DESC")) {
List<List<Integer>> ret = new ArrayList<>();
List<Integer> list = null;
String dateString = null, newString;
while (c.moveToNext()) {
Date date = new Date(c.getLong(c.getColumnIndex("time")));
newString = DateFormat.getDateInstance(DateFormat.MEDIUM, MagiskManager.locale).format(date);
if (!TextUtils.equals(dateString, newString)) {
dateString = newString;
list = new ArrayList<>();
ret.add(list);
}
list.add(c.getPosition());
}
return ret;
}
}
public Cursor getLogCursor() {
return getLogCursor(mDb);
}
public Cursor getLogCursor(SQLiteDatabase db) {
return db.query(LOG_TABLE, null, null, null, null, null, "time DESC");
}
private void migrateLegacyLogList(File oldDB, SQLiteDatabase newDB) {
try (SQLiteDatabase oldDb = SQLiteDatabase.openDatabase(oldDB.getPath(), null, SQLiteDatabase.OPEN_READWRITE);
Cursor c = getLogCursor(oldDb)) {
while (c.moveToNext()) {
ContentValues values = new ContentValues();
DatabaseUtils.cursorRowToContentValues(c, values);
newDB.insert(LOG_TABLE, null, values);
}
}
}
public void addLog(SuLogEntry log) {
mDb.insert(LOG_TABLE, null, log.getContentValues());
}
public void clearLogs() {
mDb.delete(LOG_TABLE, null, null);
}
public void setSettings(String key, int value) {
ContentValues data = new ContentValues();
data.put("key", key);
data.put("value", value);
mDb.replace(SETTINGS_TABLE, null, data);
}
public int getSettings(String key, int defaultValue) {
int value = defaultValue;
try (Cursor c = mDb.query(SETTINGS_TABLE, null, "key=?",new String[] { key }, null, null, null)) {
if (c.moveToNext()) {
value = c.getInt(c.getColumnIndex("value"));
}
}
return value;
}
}

View File

@@ -1,108 +0,0 @@
package com.topjohnwu.magisk.module;
import android.support.annotation.NonNull;
import com.topjohnwu.magisk.utils.Logger;
import java.util.List;
public abstract class BaseModule implements Comparable<BaseModule> {
protected String mId, mName, mVersion, mAuthor, mDescription, mSupportUrl, mDonateUrl;
protected boolean mIsCacheModule = false;
protected int mVersionCode = 0;
protected void parseProps(List<String> props) throws CacheModException { parseProps(props.toArray(new String[props.size()])); }
protected void parseProps(String[] props) throws CacheModException {
for (String line : props) {
String[] prop = line.split("=", 2);
if (prop.length != 2) {
continue;
}
String key = prop[0].trim();
if (key.charAt(0) == '#') {
continue;
}
switch (key) {
case "id":
this.mId = prop[1];
break;
case "name":
this.mName = prop[1];
break;
case "version":
this.mVersion = prop[1];
break;
case "versionCode":
try {
this.mVersionCode = Integer.parseInt(prop[1]);
} catch (NumberFormatException ignored) {}
break;
case "author":
this.mAuthor = prop[1];
break;
case "description":
this.mDescription = prop[1];
break;
case "support":
this.mSupportUrl = prop[1];
break;
case "donate":
this.mDonateUrl = prop[1];
break;
case "cacheModule":
this.mIsCacheModule = Boolean.parseBoolean(prop[1]);
break;
default:
break;
}
}
if (mIsCacheModule)
throw new CacheModException(mId);
}
public String getName() {
return mName;
}
public String getVersion() {
return mVersion;
}
public String getAuthor() {
return mAuthor;
}
public String getId() {return mId; }
public String getDescription() {
return mDescription;
}
public int getVersionCode() {
return mVersionCode;
}
public String getDonateUrl() {
return mDonateUrl;
}
public String getSupportUrl() {
return mSupportUrl;
}
public static class CacheModException extends Exception {
public CacheModException(String id) {
Logger.dev("Cache mods are no longer supported! id: " + id);
}
}
@Override
public int compareTo(@NonNull BaseModule o) {
return this.getName().toLowerCase().compareTo(o.getName().toLowerCase());
}
}

View File

@@ -1,62 +0,0 @@
package com.topjohnwu.magisk.module;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Utils;
public class Module extends BaseModule {
private String mRemoveFile, mDisableFile, mUpdateFile;
private boolean mEnable, mRemove, mUpdated;
public Module(String path) throws CacheModException {
parseProps(Utils.readFile(path + "/module.prop"));
mRemoveFile = path + "/remove";
mDisableFile = path + "/disable";
mUpdateFile = path + "/update";
if (mId == null) {
int sep = path.lastIndexOf('/');
mId = path.substring(sep + 1);
}
if (mName == null)
mName = mId;
Logger.dev("Creating Module, id: " + mId);
mEnable = !Utils.itemExist(mDisableFile);
mRemove = Utils.itemExist(mRemoveFile);
mUpdated = Utils.itemExist(mUpdateFile);
}
public void createDisableFile() {
mEnable = !Utils.createFile(mDisableFile);
}
public void removeDisableFile() {
mEnable = Utils.removeItem(mDisableFile);
}
public boolean isEnabled() {
return mEnable;
}
public void createRemoveFile() {
mRemove = Utils.createFile(mRemoveFile);
}
public void deleteRemoveFile() {
mRemove = !Utils.removeItem(mRemoveFile);
}
public boolean willBeRemoved() {
return mRemove;
}
public boolean isUpdated() {
return mUpdated;
}
}

View File

@@ -1,56 +0,0 @@
package com.topjohnwu.magisk.module;
import android.content.Context;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.WebRequest;
import java.util.Date;
public class Repo extends BaseModule {
private String mLogUrl, mManifestUrl, mZipUrl;
private Date mLastUpdate;
public Repo(Context context, String name, Date lastUpdate) throws CacheModException {
mLastUpdate = lastUpdate;
mLogUrl = context.getString(R.string.file_url, name, "changelog.txt");
mManifestUrl = context.getString(R.string.file_url, name, "module.prop");
mZipUrl = context.getString(R.string.zip_url, name);
update();
}
public void update() throws CacheModException {
Logger.dev("Repo: Re-fetch prop");
String props = WebRequest.makeWebServiceCall(mManifestUrl, WebRequest.GET, true);
String lines[] = props.split("\\n");
parseProps(lines);
}
public void update(Date lastUpdate) throws CacheModException {
Logger.dev("Repo: Old: " + mLastUpdate);
Logger.dev("Repo: New: " + lastUpdate);
if (mIsCacheModule)
throw new CacheModException(mId);
if (lastUpdate.after(mLastUpdate)) {
mLastUpdate = lastUpdate;
update();
}
}
public String getZipUrl() {
return mZipUrl;
}
public String getLogUrl() {
return mLogUrl;
}
public String getManifestUrl() {
return mManifestUrl;
}
public Date getLastUpdate() {
return mLastUpdate;
}
}

View File

@@ -0,0 +1,28 @@
package com.topjohnwu.magisk.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import com.topjohnwu.magisk.services.OnBootIntentService;
public class BootReceiver extends BroadcastReceiver {
private void startIntentService(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(new Intent(context, OnBootIntentService.class));
} else {
context.startService(new Intent(context, OnBootIntentService.class));
}
}
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
// There is currently no need to start an IntentService onBoot
// startIntentService(context);
}
}
}

View File

@@ -12,7 +12,6 @@ import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Utils;
public abstract class DownloadReceiver extends BroadcastReceiver {
public Context mContext;
public String mFilename;
long downloadID;
@@ -20,7 +19,6 @@ public abstract class DownloadReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
mContext = context;
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
String action = intent.getAction();
if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
@@ -36,7 +34,7 @@ public abstract class DownloadReceiver extends BroadcastReceiver {
onDownloadDone(uri);
break;
default:
Toast.makeText(context, R.string.download_file_error, Toast.LENGTH_LONG).show();
Utils.getMagiskManager(context).toast(R.string.download_file_error, Toast.LENGTH_LONG);
break;
}
context.unregisterReceiver(this);

View File

@@ -1,67 +0,0 @@
package com.topjohnwu.magisk.receivers;
import android.net.Uri;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.StatusFragment;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.Shell;
import com.topjohnwu.magisk.utils.ZipUtils;
import java.io.File;
public class MagiskDlReceiver extends DownloadReceiver {
String mBoot;
boolean mEnc, mVerity;
public MagiskDlReceiver(String bootImage, boolean keepEnc, boolean keepVerity) {
mBoot = bootImage;
mEnc = keepEnc;
mVerity = keepVerity;
}
@Override
public void onDownloadDone(Uri uri) {
new Async.FlashZIP(mContext, uri, mFilename) {
@Override
protected void preProcessing() throws Throwable {
Shell.su(
"echo \"BOOTIMAGE=/dev/block/" + mBoot + "\" > /dev/.magisk",
"echo \"KEEPFORCEENCRYPT=" + String.valueOf(mEnc) + "\" >> /dev/.magisk",
"echo \"KEEPVERITY=" + String.valueOf(mVerity) + "\" >> /dev/.magisk"
);
}
@Override
protected boolean unzipAndCheck() {
publishProgress(mContext.getString(R.string.zip_install_unzip_zip_msg));
if (Shell.rootAccess()) {
// We might not have busybox yet, unzip with Java
// We will have complete busybox after Magisk installation
ZipUtils.unzip(mCachedFile, new File(mCachedFile.getParent(), "magisk"));
Shell.su(
"mkdir -p " + Async.TMP_FOLDER_PATH + "/magisk",
"cp -af " + mCachedFile.getParent() + "/magisk/. " + Async.TMP_FOLDER_PATH + "/magisk",
"mv -f " + mCachedFile.getParent() + "/magisk/META-INF " + mCachedFile.getParent() + "/META-INF"
);
}
return true;
}
@Override
protected void onSuccess() {
new Async.RootTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Shell.su("setprop magisk.version "
+ String.valueOf(StatusFragment.remoteMagiskVersion));
return null;
}
}.exec();
super.onSuccess();
}
}.exec();
}
}

View File

@@ -0,0 +1,42 @@
package com.topjohnwu.magisk.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.FileProvider;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.utils.Utils;
import java.io.File;
public class ManagerUpdate extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Utils.dlAndReceive(
context,
new DownloadReceiver() {
@Override
public void onDownloadDone(Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri content = FileProvider.getUriForFile(context,
"com.topjohnwu.magisk.provider", new File(uri.getPath()));
install.setData(content);
context.startActivity(install);
} else {
Intent install = new Intent(Intent.ACTION_VIEW);
install.setDataAndType(uri, "application/vnd.android.package-archive");
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(install);
}
}
},
intent.getStringExtra(MagiskManager.INTENT_LINK),
Utils.getLegalFilename("MagiskManager-v" +
intent.getStringExtra(MagiskManager.INTENT_VERSION) + ".apk"));
}
}

View File

@@ -0,0 +1,40 @@
package com.topjohnwu.magisk.receivers;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.utils.Utils;
public class PackageReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
MagiskManager magiskManager = Utils.getMagiskManager(context);
String pkg = intent.getData().getEncodedSchemeSpecificPart();
Policy policy = magiskManager.suDB.getPolicy(pkg);
if (policy == null)
return;
switch (intent.getAction()) {
case Intent.ACTION_PACKAGE_REPLACED:
// This will only work pre-O
if (magiskManager.suReauth) {
magiskManager.suDB.deletePolicy(policy);
} else {
int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
// Update the UID if available
if (uid > 0) {
policy.uid = uid % 100000;
}
magiskManager.suDB.updatePolicy(policy);
}
break;
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
magiskManager.suDB.deletePolicy(policy);
break;
}
}
}

View File

@@ -1,42 +0,0 @@
package com.topjohnwu.magisk.receivers;
import android.net.Uri;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Async;
import com.topjohnwu.magisk.utils.Utils.ByteArrayInOutStream;
import com.topjohnwu.magisk.utils.ZipUtils;
import java.io.OutputStream;
public class RepoDlReceiver extends DownloadReceiver {
@Override
public void onDownloadDone(Uri uri) {
// Flash the zip
new Async.FlashZIP(mContext, uri, mFilename){
@Override
protected void preProcessing() throws Throwable {
// Process and sign the zip
publishProgress(mContext.getString(R.string.zip_install_process_zip_msg));
ByteArrayInOutStream buffer = new ByteArrayInOutStream();
// First remove top folder (the folder with the repo name) in Github source zip
ZipUtils.removeTopFolder(mContext.getContentResolver().openInputStream(mUri), buffer);
// Then sign the zip for the first time
ZipUtils.signZip(mContext, buffer.getInputStream(), buffer, false);
// Adjust the zip to prevent unzip issues
ZipUtils.adjustZip(buffer);
// Finally, sign the whole zip file again
ZipUtils.signZip(mContext, buffer.getInputStream(), buffer, true);
// Write it back to the downloaded zip
OutputStream out = mContext.getContentResolver().openOutputStream(mUri);
buffer.writeTo(out);
out.close();
}
}.exec();
}
}

View File

@@ -0,0 +1,34 @@
package com.topjohnwu.magisk.services;
import android.app.IntentService;
import android.content.Intent;
import android.os.Build;
import android.support.v7.app.NotificationCompat;
import com.topjohnwu.magisk.R;
public class OnBootIntentService extends IntentService {
private static final int ONBOOT_NOTIFICATION_ID = 3;
public OnBootIntentService() {
super("OnBootIntentService");
}
@Override
public void onCreate() {
super.onCreate();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.drawable.ic_magisk)
.setContentTitle("onBoot")
.setContentText("Running onBoot operations...");
startForeground(ONBOOT_NOTIFICATION_ID, builder.build());
}
}
@Override
protected void onHandleIntent(Intent intent) {
// Currently nothing to do
}
}

View File

@@ -0,0 +1,23 @@
package com.topjohnwu.magisk.services;
import android.app.job.JobParameters;
import android.app.job.JobService;
import com.topjohnwu.magisk.asyncs.CheckUpdates;
import com.topjohnwu.magisk.utils.Utils;
public class UpdateCheckService extends JobService {
@Override
public boolean onStartJob(JobParameters params) {
Utils.getMagiskManager(this).getMagiskInfo();
new CheckUpdates(this, true)
.setCallBack(() -> jobFinished(params, false)).exec();
return true;
}
@Override
public boolean onStopJob(JobParameters params) {
return true;
}
}

View File

@@ -0,0 +1,24 @@
package com.topjohnwu.magisk.superuser;
import android.content.Intent;
import android.os.Bundle;
import com.topjohnwu.magisk.components.Activity;
public class RequestActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent == null) {
finish();
return;
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setClass(this, SuRequestActivity.class);
startActivity(intent);
finish();
}
}

View File

@@ -0,0 +1,95 @@
package com.topjohnwu.magisk.superuser;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Process;
import android.widget.Toast;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.container.Policy;
import com.topjohnwu.magisk.container.SuLogEntry;
import java.util.Date;
public class SuReceiver extends BroadcastReceiver {
public static final int NO_NOTIFICATION = 0;
public static final int TOAST = 1;
private static final int NOTIFY_NORMAL_LOG = 0;
private static final int NOTIFY_USER_TOASTS = 1;
private static final int NOTIFY_USER_TO_OWNER = 2;
@Override
public void onReceive(Context context, Intent intent) {
int fromUid, toUid, pid, mode;
String command, action;
Policy policy;
MagiskManager magiskManager = (MagiskManager) context.getApplicationContext();
if (intent == null) return;
mode = intent.getIntExtra("mode", -1);
if (mode < 0) return;
if (mode == NOTIFY_USER_TO_OWNER) {
magiskManager.toast(R.string.multiuser_hint_owner_request, Toast.LENGTH_LONG);
return;
}
fromUid = intent.getIntExtra("from.uid", -1);
if (fromUid < 0) return;
if (fromUid == Process.myUid()) return; // Don't show anything if it's Magisk Manager
action = intent.getStringExtra("action");
if (action == null) return;
policy = magiskManager.suDB.getPolicy(fromUid);
if (policy == null) {
try {
policy = new Policy(fromUid, context.getPackageManager());
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return;
}
}
SuLogEntry log = new SuLogEntry(policy);
String message;
switch (action) {
case "allow":
message = context.getString(R.string.su_allow_toast, policy.appName);
log.action = true;
break;
case "deny":
message = context.getString(R.string.su_deny_toast, policy.appName);
log.action = false;
break;
default:
return;
}
if (policy.notification && magiskManager.suNotificationType == TOAST) {
magiskManager.toast(message, Toast.LENGTH_SHORT);
}
if (mode == NOTIFY_NORMAL_LOG && policy.logging) {
toUid = intent.getIntExtra("to.uid", -1);
if (toUid < 0) return;
pid = intent.getIntExtra("pid", -1);
if (pid < 0) return;
command = intent.getStringExtra("command");
if (command == null) return;
log.toUid = toUid;
log.fromPid = pid;
log.command = command;
log.date = new Date();
magiskManager.suDB.addLog(log);
}
}
}

View File

@@ -0,0 +1,239 @@
package com.topjohnwu.magisk.superuser;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.FileObserver;
import android.text.TextUtils;
import android.view.Window;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.asyncs.ParallelTask;
import com.topjohnwu.magisk.components.Activity;
import com.topjohnwu.magisk.container.Policy;
import java.io.DataInputStream;
import java.io.IOException;
import butterknife.BindView;
import butterknife.ButterKnife;
public class SuRequestActivity extends Activity {
public static final int PROMPT = 0;
public static final int AUTO_DENY = 1;
public static final int AUTO_ALLOW = 2;
private static final int[] timeoutList = {0, -1, 10, 20, 30, 60};
@BindView(R.id.su_popup) LinearLayout suPopup;
@BindView(R.id.timeout) Spinner timeout;
@BindView(R.id.app_icon) ImageView appIcon;
@BindView(R.id.app_name) TextView appNameView;
@BindView(R.id.package_name) TextView packageNameView;
@BindView(R.id.grant_btn) Button grant_btn;
@BindView(R.id.deny_btn) Button deny_btn;
private String socketPath;
private LocalSocket socket;
private PackageManager pm;
private MagiskManager magiskManager;
private boolean hasTimeout;
private Policy policy;
private CountDownTimer timer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
pm = getPackageManager();
magiskManager = getMagiskManager();
Intent intent = getIntent();
socketPath = intent.getStringExtra("socket");
hasTimeout = intent.getBooleanExtra("timeout", true);
new FileObserver(socketPath) {
@Override
public void onEvent(int fileEvent, String path) {
if (fileEvent == FileObserver.DELETE_SELF) {
finish();
}
}
}.startWatching();
new SocketManager(this).exec();
}
private boolean cancelTimeout() {
timer.cancel();
deny_btn.setText(getString(R.string.deny));
return false;
}
private void showRequest() {
switch (magiskManager.suResponseType) {
case AUTO_DENY:
handleAction(Policy.DENY, 0);
return;
case AUTO_ALLOW:
handleAction(Policy.ALLOW, 0);
return;
case PROMPT:
default:
}
// If not interactive, response directly
if (policy.policy != Policy.INTERACTIVE) {
handleAction();
return;
}
setContentView(R.layout.activity_request);
ButterKnife.bind(this);
appIcon.setImageDrawable(policy.info.loadIcon(pm));
appNameView.setText(policy.appName);
packageNameView.setText(policy.packageName);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
R.array.allow_timeout, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
timeout.setAdapter(adapter);
timer = new CountDownTimer(magiskManager.suRequestTimeout * 1000, 1000) {
@Override
public void onTick(long millisUntilFinished) {
deny_btn.setText(getString(R.string.deny_with_str, "(" + millisUntilFinished / 1000 + ")"));
}
@Override
public void onFinish() {
deny_btn.setText(getString(R.string.deny_with_str, "(0)"));
handleAction(Policy.DENY);
}
};
grant_btn.setOnClickListener(v -> {
handleAction(Policy.ALLOW);
timer.cancel();
});
deny_btn.setOnClickListener(v -> {
handleAction(Policy.DENY);
timer.cancel();
});
suPopup.setOnClickListener(v -> cancelTimeout());
timeout.setOnTouchListener((v, event) -> cancelTimeout());
if (hasTimeout) {
timer.start();
} else {
cancelTimeout();
}
}
@Override
public void onBackPressed() {
if (policy != null) {
handleAction(Policy.DENY);
}
finish();
}
void handleAction() {
String response;
if (policy.policy == Policy.ALLOW) {
response = "socket:ALLOW";
} else {
response = "socket:DENY";
}
try {
socket.getOutputStream().write((response).getBytes());
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
finish();
}
void handleAction(int action) {
handleAction(action, timeoutList[timeout.getSelectedItemPosition()]);
}
void handleAction(int action, int time) {
policy.policy = action;
if (time >= 0) {
policy.until = (time == 0) ? 0 : (System.currentTimeMillis() / 1000 + time * 60);
magiskManager.suDB.addPolicy(policy);
}
handleAction();
}
private class SocketManager extends ParallelTask<Void, Void, Boolean> {
SocketManager(Activity context) {
super(context);
}
@Override
protected Boolean doInBackground(Void... params) {
try {
socket = new LocalSocket();
socket.connect(new LocalSocketAddress(socketPath, LocalSocketAddress.Namespace.FILESYSTEM));
DataInputStream is = new DataInputStream(socket.getInputStream());
ContentValues payload = new ContentValues();
while (true) {
int nameLen = is.readInt();
byte[] nameBytes = new byte[nameLen];
is.readFully(nameBytes);
String name = new String(nameBytes);
if (TextUtils.equals(name, "eof"))
break;
int dataLen = is.readInt();
byte[] dataBytes = new byte[dataLen];
is.readFully(dataBytes);
String data = new String(dataBytes);
payload.put(name, data);
}
if (payload.getAsInteger("uid") == null) {
return false;
}
int uid = payload.getAsInteger("uid");
policy = magiskManager.suDB.getPolicy(uid);
if (policy == null) {
policy = new Policy(uid, pm);
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
showRequest();
} else {
finish();
}
}
}
}

View File

@@ -1,305 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.app.ProgressDialog;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.provider.OpenableColumns;
import android.util.Log;
import android.widget.Toast;
import com.topjohnwu.magisk.InstallFragment;
import com.topjohnwu.magisk.MainActivity;
import com.topjohnwu.magisk.ModulesFragment;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.ReposFragment;
import com.topjohnwu.magisk.StatusFragment;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
public class Async {
public abstract static class RootTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
@SafeVarargs
public final void exec(Params... params) {
executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params);
}
}
public abstract static class NormalTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
@SafeVarargs
public final void exec(Params... params) {
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
}
}
public static final String UPDATE_JSON = "https://raw.githubusercontent.com/topjohnwu/MagiskManager/updates/magisk_update.json";
public static final String MAGISK_HIDE_PATH = "/magisk/.core/magiskhide/";
public static final String TMP_FOLDER_PATH = "/dev/tmp";
public static class CheckUpdates extends NormalTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
String jsonStr = WebRequest.makeWebServiceCall(UPDATE_JSON, WebRequest.GET);
try {
JSONObject json = new JSONObject(jsonStr);
JSONObject magisk = json.getJSONObject("magisk");
StatusFragment.remoteMagiskVersion = magisk.getDouble("versionCode");
StatusFragment.magiskLink = magisk.getString("link");
StatusFragment.magiskChangelog = magisk.getString("changelog");
} catch (JSONException ignored) {}
return null;
}
@Override
protected void onPostExecute(Void v) {
StatusFragment.updateCheckDone.trigger();
}
}
public static void checkSafetyNet(Context context) {
new SafetyNetHelper(context) {
@Override
public void handleResults(int i) {
StatusFragment.SNCheckResult = i;
StatusFragment.safetyNetDone.trigger();
}
}.requestTest();
}
public static class LoadModules extends RootTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... voids) {
ModuleHelper.createModuleMap();
return null;
}
@Override
protected void onPostExecute(Void v) {
ModulesFragment.moduleLoadDone.trigger();
}
}
public static class LoadRepos extends NormalTask<Void, Void, Void> {
private Context mContext;
public LoadRepos(Context context) {
mContext = context;
}
@Override
protected Void doInBackground(Void... voids) {
ModuleHelper.createRepoMap(mContext);
return null;
}
@Override
protected void onPostExecute(Void v) {
ReposFragment.repoLoadDone.trigger();
}
}
public static class FlashZIP extends RootTask<Void, String, Integer> {
protected Uri mUri;
protected File mCachedFile;
private String mFilename;
protected ProgressDialog progress;
private Context mContext;
public FlashZIP(Context context, Uri uri, String filename) {
mContext = context;
mUri = uri;
mFilename = filename;
}
public FlashZIP(Context context, Uri uri) {
mContext = context;
mUri = uri;
// Try to get the filename ourselves
Cursor c = mContext.getContentResolver().query(uri, null, null, null, null);
if (c != null) {
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
c.moveToFirst();
if (nameIndex != -1) {
mFilename = c.getString(nameIndex);
}
c.close();
}
if (mFilename == null) {
int idx = uri.getPath().lastIndexOf('/');
mFilename = uri.getPath().substring(idx + 1);
}
}
protected void preProcessing() throws Throwable {}
protected void copyToCache() throws Throwable {
publishProgress(mContext.getString(R.string.copying_msg));
try {
InputStream in = mContext.getContentResolver().openInputStream(mUri);
mCachedFile = new File(mContext.getCacheDir().getAbsolutePath() + "/install.zip");
if (mCachedFile.exists() && !mCachedFile.delete()) {
throw new IOException();
}
OutputStream outputStream = new FileOutputStream(mCachedFile);
byte buffer[] = new byte[1024];
int length;
while ((length = in.read(buffer)) > 0) {
outputStream.write(buffer, 0, length);
}
outputStream.close();
Logger.dev("FlashZip: File created successfully - " + mCachedFile.getPath());
in.close();
} catch (FileNotFoundException e) {
Log.e(Logger.TAG, "FlashZip: Invalid Uri");
throw e;
} catch (IOException e) {
Log.e(Logger.TAG, "FlashZip: Error in creating file");
throw e;
}
}
protected boolean unzipAndCheck() {
ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android");
return Utils.readFile(mCachedFile.getParent() + "/META-INF/com/google/android/updater-script")
.get(0).contains("#MAGISK");
}
@Override
protected void onPreExecute() {
progress = new ProgressDialog(mContext);
progress.setTitle(R.string.zip_install_progress_title);
progress.show();
}
@Override
protected void onProgressUpdate(String... values) {
progress.setMessage(values[0]);
}
@Override
protected Integer doInBackground(Void... voids) {
Logger.dev("FlashZip Running... " + mFilename);
List<String> ret;
try {
preProcessing();
copyToCache();
} catch (Throwable e) {
e.printStackTrace();
return -1;
}
if (!unzipAndCheck()) return 0;
if (Shell.rootAccess()) {
publishProgress(mContext.getString(R.string.zip_install_progress_msg, mFilename));
ret = Shell.su(
"BOOTMODE=true sh " + mCachedFile.getParent() +
"/META-INF/com/google/android/update-binary dummy 1 " + mCachedFile.getPath(),
"if [ $? -eq 0 ]; then echo true; else echo false; fi"
);
Logger.dev("FlashZip: Console log:");
for (String line : ret) {
Logger.dev(line);
}
Shell.su(
"rm -rf " + mCachedFile.getParent() + "/*",
"rm -rf " + TMP_FOLDER_PATH
);
} else {
if (mCachedFile != null && mCachedFile.exists() && !mCachedFile.delete()) {
Utils.removeItem(mCachedFile.getPath());
}
return -1;
}
if (Boolean.parseBoolean(ret.get(ret.size() - 1))) {
return 1;
}
return -1;
}
// -1 = error, manual install; 0 = invalid zip; 1 = success
@Override
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
progress.dismiss();
switch (result) {
case -1:
Toast.makeText(mContext, mContext.getString(R.string.install_error), Toast.LENGTH_LONG).show();
Toast.makeText(mContext, mContext.getString(R.string.manual_install_1, mUri.getPath()), Toast.LENGTH_LONG).show();
Toast.makeText(mContext, mContext.getString(R.string.manual_install_2), Toast.LENGTH_LONG).show();
break;
case 0:
Toast.makeText(mContext, mContext.getString(R.string.invalid_zip), Toast.LENGTH_LONG).show();
break;
case 1:
onSuccess();
break;
}
}
protected void onSuccess() {
StatusFragment.updateCheckDone.trigger();
new LoadModules().exec();
MainActivity.alertBuilder
.setTitle(R.string.reboot_title)
.setMessage(R.string.reboot_msg)
.setPositiveButton(R.string.reboot, (dialogInterface1, i) -> Shell.sh("su -c reboot"))
.setNegativeButton(R.string.no_thanks, null)
.show();
}
}
public static class MagiskHide extends RootTask<Object, Void, Void> {
@Override
protected Void doInBackground(Object... params) {
boolean add = (boolean) params[0];
String packageName = (String) params[1];
Shell.su(MAGISK_HIDE_PATH + (add ? "add " : "rm ") + packageName);
return null;
}
public void add(CharSequence packageName) {
exec(true, packageName);
}
public void rm(CharSequence packageName) {
exec(false, packageName);
}
}
public static class GetBootBlocks extends RootTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
if (Shell.rootAccess()) {
InstallFragment.blockList = Shell.su("ls /dev/block | grep mmc");
if (InstallFragment.bootBlock == null) {
InstallFragment.bootBlock = Utils.detectBootImage();
}
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
InstallFragment.blockDetectionDone.trigger();
}
}
}

View File

@@ -1,46 +0,0 @@
package com.topjohnwu.magisk.utils;
import java.util.HashMap;
import java.util.HashSet;
public class CallbackHandler {
private static HashMap<Event, HashSet<EventListener>> listeners = new HashMap<>();
public static void register(Event event, EventListener listener) {
HashSet<EventListener> list = listeners.get(event);
if (list == null) {
list = new HashSet<>();
listeners.put(event, list);
}
list.add(listener);
}
public static void unRegister(Event event, EventListener listener) {
HashSet<EventListener> list = listeners.get(event);
if (list != null) {
list.remove(listener);
}
}
private static void triggerCallback(Event event) {
HashSet<EventListener> list = listeners.get(event);
if (list != null) {
for (EventListener listener : list) {
listener.onTrigger(event);
}
}
}
public static class Event {
public boolean isTriggered = false;
public void trigger() {
isTriggered = true;
triggerCallback(this);
}
}
public interface EventListener {
void onTrigger(Event event);
}
}

View File

@@ -2,37 +2,38 @@ package com.topjohnwu.magisk.utils;
import android.util.Log;
import com.topjohnwu.magisk.MagiskManager;
import java.util.Locale;
public class Logger {
public static final String TAG = "Magisk";
public static final String DEV_TAG = "Magisk: DEV";
public static final String DEBUG_TAG = "Magisk: DEBUG";
public static final String DEBUG_TAG = "MagiskManager";
private static final boolean SHELL_LOGGING = false;
public static boolean logShell, devLog;
public static void debug(String msg) {
Log.d(DEBUG_TAG, msg);
public static void debug(String line) {
Log.d(DEBUG_TAG, "DEBUG: " + line);
}
public static void dev(String msg, Object... args) {
if (devLog) {
if (args.length == 1 && args[0] instanceof Throwable) {
Log.d(DEV_TAG, msg, (Throwable) args[0]);
} else {
Log.d(DEV_TAG, String.format(msg, args));
}
public static void debug(String fmt, Object... args) {
debug(String.format(Locale.US, fmt, args));
}
public static void error(String line) {
Log.e(DEBUG_TAG, "ERROR: " + line);
}
public static void error(String fmt, Object... args) {
error(String.format(Locale.US, fmt, args));
}
public static void shell(String line) {
if (SHELL_LOGGING) {
Log.d(DEBUG_TAG, "SHELL: " + line);
}
}
public static void dev(String msg) {
if (devLog) {
Log.d(DEV_TAG, msg);
}
}
public static void shell(boolean root, String msg) {
if (logShell) {
Log.d(root ? "SU" : "SH", msg);
}
public static void shell(String fmt, Object... args) {
shell(String.format(Locale.US, fmt, args));
}
}

View File

@@ -1,188 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.module.BaseModule;
import com.topjohnwu.magisk.module.Module;
import com.topjohnwu.magisk.module.Repo;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class ModuleHelper {
private static final String MAGISK_PATH = "/magisk";
private static final String FILE_KEY = "RepoMap";
private static final String REPO_KEY = "repomap";
private static final String VERSION_KEY = "version";
private static final int DATABASE_VER = 1;
private static ValueSortedMap<String, Repo> repoMap = new ValueSortedMap<>();
private static ValueSortedMap<String, Module> moduleMap = new ValueSortedMap<>();
public static void createModuleMap() {
Logger.dev("ModuleHelper: Loading modules");
moduleMap.clear();
for (String path : Utils.getModList(MAGISK_PATH)) {
Logger.dev("ModuleHelper: Adding modules from " + path);
Module module;
try {
module = new Module(path);
moduleMap.put(module.getId(), module);
} catch (BaseModule.CacheModException ignored) {}
}
Logger.dev("ModuleHelper: Module load done");
}
public static void createRepoMap(Context context) {
Logger.dev("ModuleHelper: Loading repos");
repoMap.clear();
Gson gson = new Gson();
SharedPreferences prefs = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE);
String jsonString;
int cachedVersion = prefs.getInt(VERSION_KEY, 0);
if (cachedVersion != DATABASE_VER) {
// Ignore incompatible cached database
jsonString = null;
} else {
jsonString = prefs.getString(REPO_KEY, null);
}
ValueSortedMap<String, Repo> cached = null;
if (jsonString != null) {
cached = gson.fromJson(jsonString, new TypeToken< ValueSortedMap<String, Repo> >(){}.getType());
}
if (cached == null) {
cached = new ValueSortedMap<>();
}
// Making a request to url and getting response
jsonString = WebRequest.makeWebServiceCall(context.getString(R.string.url_main, Utils.getToken()), WebRequest.GET);
if (jsonString != null && !jsonString.isEmpty()) {
// Have internet access
try {
JSONArray jsonArray = new JSONArray(jsonString);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonobject = jsonArray.getJSONObject(i);
String id = jsonobject.getString("description");
String name = jsonobject.getString("name");
String lastUpdate = jsonobject.getString("pushed_at");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
Date updatedDate;
try {
updatedDate = format.parse(lastUpdate);
} catch (ParseException e) {
continue;
}
Repo repo = cached.get(id);
try {
if (repo == null) {
Logger.dev("ModuleHelper: Create new repo " + id);
repo = new Repo(context, name, updatedDate);
} else {
Logger.dev("ModuleHelper: Cached repo " + id);
repo.update(updatedDate);
}
if (repo.getId() != null) {
repoMap.put(id, repo);
}
} catch (BaseModule.CacheModException ignored) {}
}
} catch (JSONException e) {
e.printStackTrace();
}
} else {
// Use cached if no internet
repoMap.putAll(cached);
}
prefs.edit()
.putInt(VERSION_KEY, DATABASE_VER)
.putString(REPO_KEY, gson.toJson(repoMap))
.apply();
Logger.dev("ModuleHelper: Repo load done");
}
public static void getModuleList(List<Module> moduleList) {
moduleList.clear();
moduleList.addAll(moduleMap.values());
}
public static void getRepoLists(List<Repo> update, List<Repo> installed, List<Repo> others) {
update.clear();
installed.clear();
others.clear();
for (Repo repo : repoMap.values()) {
Module module = moduleMap.get(repo.getId());
if (module != null) {
if (repo.getVersionCode() > module.getVersionCode()) {
update.add(repo);
} else {
installed.add(repo);
}
} else {
others.add(repo);
}
}
}
private static class ValueSortedMap<K, V extends Comparable > extends HashMap<K, V> {
private List<V> sorted = new ArrayList<>();
@NonNull
@Override
public Collection<V> values() {
if (sorted.isEmpty()) {
sorted.addAll(super.values());
Collections.sort(sorted);
}
return sorted;
}
@Override
public V put(K key, V value) {
sorted.clear();
return super.put(key, value);
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
sorted.clear();
super.putAll(m);
}
@Override
public V remove(Object key) {
sorted.clear();
return super.remove(key);
}
}
}

View File

@@ -1,81 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Base64;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.Status;
import com.google.android.gms.safetynet.SafetyNet;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.SecureRandom;
public abstract class SafetyNetHelper
implements GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks {
private GoogleApiClient mGoogleApiClient;
public SafetyNetHelper(Context context) {
mGoogleApiClient = new GoogleApiClient.Builder(context)
.addApi(SafetyNet.API)
.addConnectionCallbacks(this)
.addOnConnectionFailedListener(this)
.build();
}
@Override
public void onConnectionFailed(@NonNull ConnectionResult result) {
Logger.dev("SN: Google API fail");
}
@Override
public void onConnected(@Nullable Bundle bundle) {
Logger.dev("SN: Google API Connected");
safetyNetCheck();
}
@Override
public void onConnectionSuspended(int i) {
Logger.dev("SN: Google API Suspended");
}
public void requestTest() {
// Connect Google Service
mGoogleApiClient.connect();
}
private void safetyNetCheck() {
// Create nonce
byte[] nonce = new byte[24];
new SecureRandom().nextBytes(nonce);
Logger.dev("SN: Check with nonce: " + Base64.encodeToString(nonce, Base64.DEFAULT));
// Call SafetyNet
SafetyNet.SafetyNetApi.attest(mGoogleApiClient, nonce)
.setResultCallback(result -> {
Status status = result.getStatus();
if (status.isSuccess()) {
String json = new String(Base64.decode(result.getJwsResult().split("\\.")[1], Base64.DEFAULT));
Logger.dev("SN: Response: " + json);
try {
JSONObject decoded = new JSONObject(json);
handleResults(decoded.getBoolean("ctsProfileMatch") ? 1 : 0);
} catch (JSONException ignored) {}
} else {
Logger.dev("SN: No response");
handleResults(-1);
}
// Disconnect
mGoogleApiClient.disconnect();
});
}
public abstract void handleResults(int i);
}

View File

@@ -1,9 +1,18 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.text.TextUtils;
import com.topjohnwu.magisk.MagiskManager;
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Collection;
import java.util.List;
/**
@@ -15,188 +24,202 @@ public class Shell {
// -1 = problematic/unknown issue; 0 = not rooted; 1 = properly rooted
public static int rootStatus;
private static Process rootShell;
private static DataOutputStream rootSTDIN;
private static StreamGobbler rootSTDOUT;
private static List<String> rootOutList = new ArrayList<>();
private final Process shellProcess;
private final DataOutputStream STDIN;
private final DataInputStream STDOUT;
static {
init();
private boolean isValid;
private void testRootShell(DataOutputStream in, DataInputStream out) throws IOException {
in.write(("id\n").getBytes("UTF-8"));
in.flush();
String s = new BufferedReader(new InputStreamReader(out)).readLine();
if (TextUtils.isEmpty(s) || !s.contains("uid=0")) {
in.close();
out.close();
throw new IOException();
}
}
private static void init() {
private Shell() {
rootStatus = 1;
Process process = null;
DataOutputStream in = null;
DataInputStream out = null;
try {
rootShell = Runtime.getRuntime().exec("su");
rootStatus = 1;
} catch (IOException err) {
// No root
rootStatus = 0;
return;
// Try getting global namespace
process = Runtime.getRuntime().exec("su --mount-master");
in = new DataOutputStream(process.getOutputStream());
out = new DataInputStream(process.getInputStream());
testRootShell(in, out);
} catch (IOException e) {
// Feature not implemented, normal root shell
try {
process = Runtime.getRuntime().exec("su");
in = new DataOutputStream(process.getOutputStream());
out = new DataInputStream(process.getInputStream());
testRootShell(in, out);
} catch (IOException e1) {
rootStatus = 0;
}
}
rootSTDIN = new DataOutputStream(rootShell.getOutputStream());
rootSTDOUT = new StreamGobbler(rootShell.getInputStream(), rootOutList, true);
rootSTDOUT.start();
// Setup umask and PATH
su("umask 022");
su("PATH=/data/busybox:$PATH");
List<String> ret = su("echo -BOC-", "id");
if (ret == null) {
// Something wrong with root, not allowed?
rootStatus = -1;
return;
}
for (String line : ret) {
if (line.contains("uid=")) {
// id command is working, let's see if we are actually root
rootStatus = line.contains("uid=0") ? rootStatus : -1;
return;
} else if (!line.contains("-BOC-")) {
rootStatus = -1;
if (!rootAccess()) {
// Try to gain non-root sh
try {
process = Runtime.getRuntime().exec("sh");
in = new DataOutputStream(process.getOutputStream());
out = new DataInputStream(process.getInputStream());
} catch (IOException e) {
// Nothing works....
shellProcess = null;
STDIN = null;
STDOUT = null;
isValid = false;
return;
}
}
isValid = true;
shellProcess = process;
STDIN = in;
STDOUT = out;
sh_raw("umask 022");
}
private Shell(String command) {
Process process;
DataOutputStream in;
DataInputStream out;
try {
process = Runtime.getRuntime().exec(command);
in = new DataOutputStream(process.getOutputStream());
out = new DataInputStream(process.getInputStream());
} catch (IOException e) {
// Nothing works....
shellProcess = null;
STDIN = null;
STDOUT = null;
isValid = false;
return;
}
isValid = true;
shellProcess = process;
STDIN = in;
STDOUT = out;
}
public static Shell getShell() {
return new Shell();
}
public static Shell getShell(String command) {
return new Shell(command);
}
public static Shell getShell(Context context) {
MagiskManager magiskManager = Utils.getMagiskManager(context);
if (magiskManager.shell == null || !magiskManager.shell.isValid) {
// Get new shell if needed
magiskManager.shell = getShell();
}
return magiskManager.shell;
}
public static boolean rootAccess() {
return rootStatus > 0;
}
public static List<String> sh(String... commands) {
List<String> res = Collections.synchronizedList(new ArrayList<String>());
public void loadInputStream(InputStream in) {
try {
Process process = Runtime.getRuntime().exec("sh");
DataOutputStream STDIN = new DataOutputStream(process.getOutputStream());
StreamGobbler STDOUT = new StreamGobbler(process.getInputStream(), res);
STDOUT.start();
try {
for (String write : commands) {
STDIN.write((write + "\n").getBytes("UTF-8"));
STDIN.flush();
Logger.shell(false, write);
}
STDIN.write("exit\n".getBytes("UTF-8"));
STDIN.flush();
} catch (IOException e) {
if (!e.getMessage().contains("EPIPE")) {
throw e;
}
int read;
byte[] bytes = new byte[4096];
while ((read = in.read(bytes)) != -1) {
STDIN.write(bytes, 0, read);
}
process.waitFor();
try {
STDIN.close();
} catch (IOException e) {
// might be closed already
}
STDOUT.join();
process.destroy();
} catch (IOException | InterruptedException e) {
// shell probably not found
res = null;
STDIN.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public List<String> sh(String... commands) {
List<String> res = new ArrayList<>();
if (!isValid) return res;
sh(res, commands);
return res;
}
// Run with the same shell by default
public static List<String> su(String... commands) {
return su(false, commands);
public void sh_raw(String... commands) {
sh_raw(false, commands);
}
public static List<String> su(boolean newShell, String... commands) {
List<String> res;
Process process;
DataOutputStream STDIN;
StreamGobbler STDOUT;
if (!rootAccess()) {
return null;
}
if (newShell) {
res = Collections.synchronizedList(new ArrayList<String>());
public void sh_raw(boolean stdout, String... commands) {
if (!isValid) return;
synchronized (shellProcess) {
try {
process = Runtime.getRuntime().exec("su");
STDIN = new DataOutputStream(process.getOutputStream());
STDOUT = new StreamGobbler(process.getInputStream(), res);
} catch (IOException err) {
return null;
for (String command : commands) {
Logger.shell(command);
STDIN.write((command + (stdout ? "\n" : " >/dev/null\n")).getBytes("UTF-8"));
STDIN.flush();
}
} catch (IOException e) {
e.printStackTrace();
shellProcess.destroy();
isValid = false;
}
STDOUT.start();
} else {
process = rootShell;
STDIN = rootSTDIN;
STDOUT = rootSTDOUT;
res = rootOutList;
res.clear();
}
}
public void sh(Collection<String> output, String... commands) {
if (!isValid) return;
try {
for (String write : commands) {
STDIN.write((write + "\n").getBytes("UTF-8"));
STDIN.flush();
Logger.shell(true, write);
}
if (newShell) {
STDIN.write("exit\n".getBytes("UTF-8"));
STDIN.flush();
process.waitFor();
shellProcess.exitValue();
isValid = false;
return; // The process is dead, return
} catch (IllegalThreadStateException ignored) {
// This should be the expected result
}
synchronized (shellProcess) {
StreamGobbler out = new StreamGobbler(STDOUT, output);
out.start();
sh_raw(true, commands);
sh_raw(true, "echo \'-shell-done-\'");
try { out.join(); } catch (InterruptedException ignored) {}
}
}
try {
STDIN.close();
} catch (IOException ignore) {
// might be closed already
}
public List<String> su(String... commands) {
if (!rootAccess()) return sh();
return sh(commands);
}
STDOUT.join();
process.destroy();
} else {
STDIN.write(("echo\n").getBytes("UTF-8"));
STDIN.flush();
STDIN.write(("echo \'-root-done-\'\n").getBytes("UTF-8"));
STDIN.flush();
while (true) {
try {
// Process terminated, it means the interactive shell has some issues
process.exitValue();
return null;
} catch (IllegalThreadStateException e) {
// Process still running, gobble output until done
int end = res.size() - 1;
if (end > 0) {
if (res.get(end).equals("-root-done-")) {
res.remove(end);
if (res.get(end -1).isEmpty()) {
res.remove(end -1);
}
break;
}
}
try { STDOUT.join(100); } catch (InterruptedException err) { return null; }
}
}
}
} catch (IOException e) {
if (!e.getMessage().contains("EPIPE")) {
Logger.dev("Shell: Root shell error...");
return null;
}
} catch(InterruptedException e) {
Logger.dev("Shell: Root shell error...");
public void su_raw(String... commands) {
if (!rootAccess()) return;
sh_raw(commands);
}
public void su(Collection<String> output, String... commands) {
if (!rootAccess()) return;
sh(output, commands);
}
public static abstract class AbstractList<E> extends java.util.AbstractList<E> {
@Override
public abstract boolean add(E e);
@Override
public E get(int i) {
return null;
}
return new ArrayList<>(res);
@Override
public int size() {
return 0;
}
}
}

View File

@@ -1,10 +1,12 @@
package com.topjohnwu.magisk.utils;
import android.text.TextUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Collection;
/**
* Modified by topjohnwu, based on Chainfire's libsuperuser
@@ -13,40 +15,38 @@ import java.util.List;
public class StreamGobbler extends Thread {
private BufferedReader reader = null;
private List<String> writer = null;
private boolean isRoot = false;
private Collection<String> writer = null;
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* <p>We use this class because sh STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param inputStream InputStream to read from
* @param outputList {@literal List<String>} to write to, or null
*/
public StreamGobbler(InputStream inputStream, List<String> outputList) {
public StreamGobbler(InputStream inputStream, Collection<String> outputList) {
try {
while (inputStream.available() != 0) {
inputStream.skip(inputStream.available());
}
} catch (IOException ignored) {}
reader = new BufferedReader(new InputStreamReader(inputStream));
writer = outputList;
}
public StreamGobbler(InputStream inputStream, List<String> outputList, boolean root) {
reader = new BufferedReader(new InputStreamReader(inputStream));
writer = outputList;
isRoot = root;
}
@Override
public void run() {
// keep reading the InputStream until it ends (or an error occurs)
try {
String line;
while ((line = reader.readLine()) != null) {
if (TextUtils.equals(line, "-shell-done-"))
return;
writer.add(line);
if (!line.equals("-root-done-") && !line.isEmpty()) {
Logger.shell(isRoot, "OUT: " + line);
}
Logger.shell(line);
}
} catch (IOException e) {
// reader probably closed, expected exit condition

View File

@@ -0,0 +1,75 @@
package com.topjohnwu.magisk.utils;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
public class Topic {
public boolean hasPublished = false;
private List<WeakReference<Subscriber>> subscribers;
public void subscribe(Subscriber sub) {
if (subscribers == null) {
subscribers = new LinkedList<>();
}
subscribers.add(new WeakReference<>(sub));
}
public void unsubscribe() {
subscribers = null;
}
public void unsubscribe(Subscriber sub) {
for (Iterator<WeakReference<Subscriber>> i = subscribers.iterator(); i.hasNext();) {
WeakReference<Subscriber> subscriber = i.next();
if (subscriber.get() == null || subscriber.get() == sub) {
i.remove();
}
}
}
public void publish() {
publish(true, null);
}
public void publish(boolean record) {
publish(record, null);
}
public void publish(boolean record, Object result) {
hasPublished = record;
if (subscribers != null) {
for (WeakReference<Subscriber> subscriber : subscribers) {
if (subscriber.get() != null)
subscriber.get().onTopicPublished(this, result);
}
}
}
public interface Subscriber {
default void subscribeTopics() {
for (Topic topic : getSubscription()) {
if (topic.hasPublished) {
onTopicPublished(topic);
}
topic.subscribe(this);
}
}
default void unsubscribeTopics() {
for (Topic event : getSubscription()) {
event.unsubscribe(this);
}
}
default void onTopicPublished() {
onTopicPublished(null, null);
}
default void onTopicPublished(Topic topic) {
onTopicPublished(topic, null);
}
void onTopicPublished(Topic topic, Object result);
Topic[] getSubscription();
}
}

View File

@@ -1,146 +1,120 @@
package com.topjohnwu.magisk.utils;
import android.Manifest;
import android.app.Activity;
import android.app.DownloadManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.OpenableColumns;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.util.Base64;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.text.TextUtils;
import android.widget.Toast;
import com.topjohnwu.magisk.FlashActivity;
import com.topjohnwu.magisk.MagiskFragment;
import com.topjohnwu.magisk.MagiskManager;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.SplashActivity;
import com.topjohnwu.magisk.asyncs.RestoreStockBoot;
import com.topjohnwu.magisk.asyncs.UpdateRepos;
import com.topjohnwu.magisk.components.AlertDialogBuilder;
import com.topjohnwu.magisk.components.SnackbarMaker;
import com.topjohnwu.magisk.receivers.DownloadReceiver;
import com.topjohnwu.magisk.receivers.ManagerUpdate;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.util.Locale;
public class Utils {
public static final int SELECT_BOOT_IMG = 3;
public static final String UNINSTALLER = "magisk_uninstaller.sh";
public static final String UTIL_FUNCTIONS= "util_functions.sh";
public static boolean isDownloading = false;
private static final String cryptoPass = "MagiskRox666";
private static final String secret = "GTYybRBTYf5his9kQ16ZNO7qgkBJ/5MyVe4CGceAOIoXgSnnk8FTd4F1dE9p5Eus";
private static final int MAGISK_UPDATE_NOTIFICATION_ID = 1;
private static final int APK_UPDATE_NOTIFICATION_ID = 2;
public static boolean itemExist(String path) {
return itemExist(true, path);
public static boolean itemExist(Shell shell, String path) {
String command = "[ -e " + path + " ] && echo true || echo false";
List<String> ret = shell.su(command);
return isValidShellResponse(ret) && Boolean.parseBoolean(ret.get(0));
}
public static boolean itemExist(boolean root, String path) {
String command = "if [ -e " + path + " ]; then echo true; else echo false; fi";
if (Shell.rootAccess() && root) {
return Boolean.parseBoolean(Shell.su(command).get(0));
} else {
return new File(path).exists();
}
}
public static boolean commandExists(String s) {
List<String> ret;
String command = "if [ -z $(which " + s + ") ]; then echo false; else echo true; fi";
ret = Shell.sh(command);
return Boolean.parseBoolean(ret.get(0));
}
public static boolean createFile(String path) {
public static void createFile(Shell shell, String path) {
String folder = path.substring(0, path.lastIndexOf('/'));
String command = "mkdir -p " + folder + " 2>/dev/null; touch " + path + " 2>/dev/null; if [ -f \"" + path + "\" ]; then echo true; else echo false; fi";
return Shell.rootAccess() && Boolean.parseBoolean(Shell.su(command).get(0));
String command = "mkdir -p " + folder + " 2>/dev/null; touch " + path + " 2>/dev/null;";
shell.su_raw(command);
}
public static boolean removeItem(String path) {
String command = "rm -rf " + path + " 2>/dev/null; if [ -e " + path + " ]; then echo false; else echo true; fi";
return Shell.rootAccess() && Boolean.parseBoolean(Shell.su(command).get(0));
public static void removeItem(Shell shell, String path) {
String command = "rm -rf " + path + " 2>/dev/null";
shell.su_raw(command);
}
public static List<String> getModList(String path) {
List<String> ret;
String command = "find " + path + " -type d -maxdepth 1 ! -name \"*.core\" ! -name \"*lost+found\" ! -name \"*magisk\"";
if (Shell.rootAccess()) {
ret = Shell.su(command);
} else {
ret = Shell.sh(command);
}
return ret;
public static List<String> getModList(Shell shell, String path) {
String command = "ls -d " + path + "/* | grep -v lost+found";
return shell.su(command);
}
public static List<String> readFile(String path) {
List<String> ret;
String command = "cat " + path;
if (Shell.rootAccess()) {
ret = Shell.su(command);
} else {
ret = Shell.sh(command);
}
return ret;
public static List<String> readFile(Shell shell, String path) {
String command = "cat " + path + " | sed '$a\\ ' | sed '$d'";
return shell.su(command);
}
public static void dlAndReceive(Context context, DownloadReceiver receiver, String link, String filename) {
if (isDownloading) {
if (isDownloading)
return;
}
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
return;
}
runWithPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> {
File file = new File(Environment.getExternalStorageDirectory() + "/MagiskManager/" + filename);
File file = new File(Environment.getExternalStorageDirectory() + "/MagiskManager/" + filename);
if ((!file.getParentFile().exists() && !file.getParentFile().mkdirs())
|| (file.exists() && !file.delete())) {
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
return;
}
if ((!file.getParentFile().exists() && !file.getParentFile().mkdirs()) || (file.exists() && !file.delete())) {
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(context, context.getString(R.string.downloading_toast, filename), Toast.LENGTH_LONG).show();
isDownloading = true;
Toast.makeText(context, context.getString(R.string.downloading_toast, filename), Toast.LENGTH_LONG).show();
isDownloading = true;
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(link));
request.setDestinationUri(Uri.fromFile(file));
receiver.setDownloadID(downloadManager.enqueue(request));
receiver.setFilename(filename);
context.registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
public static String getToken() {
try {
DESKeySpec keySpec = new DESKeySpec(cryptoPass.getBytes("UTF8"));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(keySpec);
byte[] encrypedPwdBytes = Base64.decode(secret, Base64.DEFAULT);
// cipher is not thread safe
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] decrypedValueBytes = (cipher.doFinal(encrypedPwdBytes));
return new String(decrypedValueBytes);
} catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException
| BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException
| InvalidKeySpecException e) {
e.printStackTrace();
}
return secret;
if (link != null) {
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(link));
request.setDestinationUri(Uri.fromFile(file));
receiver.setDownloadID(downloadManager.enqueue(request));
}
receiver.setFilename(filename);
context.getApplicationContext().registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
});
}
public static String getLegalFilename(CharSequence filename) {
@@ -149,32 +123,412 @@ public class Utils {
.replace("#", "").replace("@", "").replace("*", "");
}
public static String detectBootImage() {
String[] commands = {
"for PARTITION in kern-a KERN-A android_boot ANDROID_BOOT kernel KERNEL boot BOOT lnx LNX; do",
"BOOTIMAGE=`readlink /dev/block/by-name/$PARTITION || readlink /dev/block/platform/*/by-name/$PARTITION || readlink /dev/block/platform/*/*/by-name/$PARTITION`",
"if [ ! -z \"$BOOTIMAGE\" ]; then break; fi",
"done",
"echo \"${BOOTIMAGE##*/}\""
};
List<String> ret = Shell.su(commands);
if (!ret.isEmpty()) {
return ret.get(0);
}
return null;
public static boolean lowercaseContains(CharSequence string, CharSequence nonNullLowercaseSearch) {
return !TextUtils.isEmpty(string) && string.toString().toLowerCase().contains(nonNullLowercaseSearch);
}
public static class ByteArrayInOutStream extends ByteArrayOutputStream {
public ByteArrayInputStream getInputStream() {
ByteArrayInputStream in = new ByteArrayInputStream(buf, 0, count);
count = 0;
buf = new byte[32];
return in;
public static boolean isValidShellResponse(List<String> list) {
if (list != null && list.size() != 0) {
// Check if all empty
for (String res : list) {
if (!TextUtils.isEmpty(res)) return true;
}
}
public void setBuffer(byte[] buffer) {
buf = buffer;
count = buffer.length;
return false;
}
public static int getPrefsInt(SharedPreferences prefs, String key, int def) {
return Integer.parseInt(prefs.getString(key, String.valueOf(def)));
}
public static int getPrefsInt(SharedPreferences prefs, String key) {
return getPrefsInt(prefs, key, 0);
}
public static MagiskManager getMagiskManager(Context context) {
return (MagiskManager) context.getApplicationContext();
}
public static void clearRepoCache(Context context) {
MagiskManager mm = getMagiskManager(context);
mm.prefs.edit().remove(UpdateRepos.ETAG_KEY).apply();
mm.repoDB.clearRepo();
mm.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT);
}
public static String getNameFromUri(Context context, Uri uri) {
String name = null;
try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) {
if (c != null) {
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (nameIndex != -1) {
c.moveToFirst();
name = c.getString(nameIndex);
}
}
}
if (name == null) {
int idx = uri.getPath().lastIndexOf('/');
name = uri.getPath().substring(idx + 1);
}
return name;
}
public static void showUriSnack(Activity activity, Uri uri) {
SnackbarMaker.make(activity, activity.getString(R.string.internal_storage,
"/MagiskManager/" + Utils.getNameFromUri(activity, uri)),
Snackbar.LENGTH_LONG)
.setAction(R.string.ok, (v)->{}).show();
}
public static boolean checkNetworkStatus(Context context) {
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
public static void showMagiskUpdateNotification(MagiskManager mm) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, MagiskManager.NOTIFICATION_CHANNEL);
builder.setSmallIcon(R.drawable.ic_magisk)
.setContentTitle(mm.getString(R.string.magisk_update_title))
.setContentText(mm.getString(R.string.magisk_update_available, mm.remoteMagiskVersionString))
.setVibrate(new long[]{0, 100, 100, 100})
.setAutoCancel(true);
Intent intent = new Intent(mm, SplashActivity.class);
intent.putExtra(MagiskManager.INTENT_SECTION, "magisk");
TaskStackBuilder stackBuilder = TaskStackBuilder.create(mm);
stackBuilder.addParentStack(SplashActivity.class);
stackBuilder.addNextIntent(intent);
PendingIntent pendingIntent = stackBuilder.getPendingIntent(MAGISK_UPDATE_NOTIFICATION_ID,
PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(MAGISK_UPDATE_NOTIFICATION_ID, builder.build());
}
public static void showManagerUpdateNotification(MagiskManager mm) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(mm, MagiskManager.NOTIFICATION_CHANNEL);
builder.setSmallIcon(R.drawable.ic_magisk)
.setContentTitle(mm.getString(R.string.manager_update_title))
.setContentText(mm.getString(R.string.manager_download_install))
.setVibrate(new long[]{0, 100, 100, 100})
.setAutoCancel(true);
Intent intent = new Intent(mm, ManagerUpdate.class);
intent.putExtra(MagiskManager.INTENT_LINK, mm.managerLink);
intent.putExtra(MagiskManager.INTENT_VERSION, mm.remoteManagerVersionString);
PendingIntent pendingIntent = PendingIntent.getBroadcast(mm,
APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(pendingIntent);
NotificationManager notificationManager =
(NotificationManager) mm.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(APK_UPDATE_NOTIFICATION_ID, builder.build());
}
public static void enableMagiskHide(Shell shell) {
shell.su_raw("magiskhide --enable");
}
public static void disableMagiskHide(Shell shell) {
shell.su_raw("magiskhide --disable");
}
public static List<String> listMagiskHide(Shell shell) {
return shell.su("magiskhide --ls");
}
public static void addMagiskHide(Shell shell, String pkg) {
shell.su_raw("magiskhide --add " + pkg);
}
public static void rmMagiskHide(Shell shell, String pkg) {
shell.su_raw("magiskhide --rm " + pkg);
}
public static String getLocaleString(Context context, Locale locale, @StringRes int id) {
Configuration config = context.getResources().getConfiguration();
config.setLocale(locale);
Context localizedContext = context.createConfigurationContext(config);
return localizedContext.getString(id);
}
public static List<Locale> getAvailableLocale(Context context) {
List<Locale> locales = new ArrayList<>();
HashSet<String> set = new HashSet<>();
Locale locale;
int compareId = R.string.download_file_error;
// Add default locale
locales.add(Locale.ENGLISH);
set.add(getLocaleString(context, Locale.ENGLISH, compareId));
// Add some special locales
locales.add(Locale.TAIWAN);
set.add(getLocaleString(context, Locale.TAIWAN, compareId));
locale = new Locale("pt", "BR");
locales.add(locale);
set.add(getLocaleString(context, locale, compareId));
// Other locales
for (String s : context.getAssets().getLocales()) {
locale = Locale.forLanguageTag(s);
if (set.add(getLocaleString(context, locale, compareId))) {
locales.add(locale);
}
}
Collections.sort(locales, (l1, l2) -> l1.getDisplayName(l1).compareTo(l2.getDisplayName(l2)));
return locales;
}
public static String genPackageName(String prefix, int length) {
StringBuilder builder = new StringBuilder(length);
builder.append(prefix);
length -= prefix.length();
SecureRandom random = new SecureRandom();
String base = "abcdefghijklmnopqrstuvwxyz";
String alpha = base + base.toUpperCase();
String full = alpha + "0123456789..........";
char next, prev = '\0';
for (int i = 0; i < length; ++i) {
if (prev == '.' || i == length - 1 || i == 0) {
next = alpha.charAt(random.nextInt(alpha.length()));
} else {
next = full.charAt(random.nextInt(full.length()));
}
builder.append(next);
prev = next;
}
return builder.toString();
}
public static void runWithPermission(Context context, String permission, Runnable callback) {
if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) {
// Passed in context should be an activity if not granted, need to show dialog!
if (!(context instanceof com.topjohnwu.magisk.components.Activity))
return;
com.topjohnwu.magisk.components.Activity activity = (com.topjohnwu.magisk.components.Activity) context;
activity.setPermissionGrantCallback(callback);
ActivityCompat.requestPermissions(activity, new String[] { permission }, 0);
} else {
callback.run();
}
}
public static void showMagiskInstallDialog(MagiskFragment fragment, boolean enc, boolean verity) {
MagiskManager mm = getMagiskManager(fragment.getActivity());
String filename = getLegalFilename("Magisk-v" + mm.remoteMagiskVersionString + ".zip");
new AlertDialogBuilder(fragment.getActivity())
.setTitle(mm.getString(R.string.repo_install_title, mm.getString(R.string.magisk)))
.setMessage(mm.getString(R.string.repo_install_msg, filename))
.setCancelable(true)
.setPositiveButton(R.string.install, (d, i) -> {
List<String> options = new ArrayList<>();
options.add(mm.getString(R.string.download_zip_only));
options.add(mm.getString(R.string.patch_boot_file));
if (Shell.rootAccess()) {
options.add(mm.getString(R.string.direct_install));
}
List<String> res = Shell.getShell(mm).su("echo $SLOT");
if (isValidShellResponse(res)) {
options.add(mm.getString(R.string.install_second_slot));
}
char[] slot = isValidShellResponse(res) ? res.get(0).toCharArray() : null;
new AlertDialog.Builder(fragment.getActivity())
.setTitle(R.string.select_method)
.setItems(
options.toArray(new String [0]),
(dialog, idx) -> {
String boot;
DownloadReceiver receiver = null;
switch (idx) {
case 1:
if (mm.remoteMagiskVersionCode < 1400) {
mm.toast(R.string.no_boot_file_patch_support, Toast.LENGTH_LONG);
return;
}
mm.toast(R.string.boot_file_patch_msg, Toast.LENGTH_LONG);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
fragment.startActivityForResult(intent, SELECT_BOOT_IMG,
(requestCode, resultCode, data) -> {
if (requestCode == SELECT_BOOT_IMG
&& resultCode == Activity.RESULT_OK && data != null) {
dlAndReceive(
fragment.getActivity(),
new DownloadReceiver() {
@Override
public void onDownloadDone(Uri uri) {
Intent intent = new Intent(mm, FlashActivity.class);
intent.setData(uri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(FlashActivity.SET_BOOT, data.getData())
.putExtra(FlashActivity.SET_ENC, enc)
.putExtra(FlashActivity.SET_VERITY, verity)
.putExtra(FlashActivity.SET_ACTION, FlashActivity.PATCH_BOOT);
mm.startActivity(intent);
}
},
mm.magiskLink,
filename
);
}
});
return;
case 0:
receiver = new DownloadReceiver() {
@Override
public void onDownloadDone(Uri uri) {
showUriSnack(fragment.getActivity(), uri);
}
};
break;
case 2:
boot = fragment.getSelectedBootImage();
if (boot == null)
return;
receiver = new DownloadReceiver() {
@Override
public void onDownloadDone(Uri uri) {
Intent intent = new Intent(mm, FlashActivity.class);
intent.setData(uri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(FlashActivity.SET_BOOT, boot)
.putExtra(FlashActivity.SET_ENC, enc)
.putExtra(FlashActivity.SET_VERITY, verity)
.putExtra(FlashActivity.SET_ACTION, FlashActivity.FLASH_MAGISK);
mm.startActivity(intent);
}
};
break;
case 3:
assert (slot != null);
// Choose the other slot
if (slot[1] == 'a') slot[1] = 'b';
else slot[1] = 'a';
// Then find the boot image again
List<String> ret = Shell.getShell(mm).su(
"BOOTIMAGE=",
"SLOT=" + String.valueOf(slot),
"find_boot_image",
"echo \"$BOOTIMAGE\""
);
boot = isValidShellResponse(ret) ? ret.get(ret.size() - 1) : null;
Shell.getShell(mm).su_raw("mount_partitions");
if (boot == null)
return;
receiver = new DownloadReceiver() {
@Override
public void onDownloadDone(Uri uri) {
Intent intent = new Intent(mm, FlashActivity.class);
intent.setData(uri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(FlashActivity.SET_BOOT, boot)
.putExtra(FlashActivity.SET_ENC, enc)
.putExtra(FlashActivity.SET_VERITY, verity)
.putExtra(FlashActivity.SET_ACTION, FlashActivity.FLASH_MAGISK);
mm.startActivity(intent);
}
};
default:
}
Utils.dlAndReceive(
fragment.getActivity(),
receiver,
mm.magiskLink,
filename
);
}
).show();
})
.setNeutralButton(R.string.release_notes, (d, i) -> {
if (mm.releaseNoteLink != null) {
Intent openLink = new Intent(Intent.ACTION_VIEW, Uri.parse(mm.releaseNoteLink));
openLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mm.startActivity(openLink);
}
})
.setNegativeButton(R.string.no_thanks, null)
.show();
}
public static void showManagerInstallDialog(Activity activity) {
MagiskManager mm = Utils.getMagiskManager(activity);
new AlertDialogBuilder(activity)
.setTitle(mm.getString(R.string.repo_install_title, mm.getString(R.string.app_name)))
.setMessage(mm.getString(R.string.repo_install_msg,
Utils.getLegalFilename("MagiskManager-v" +
mm.remoteManagerVersionString + ".apk")))
.setCancelable(true)
.setPositiveButton(R.string.install, (d, i) -> {
Intent intent = new Intent(mm, ManagerUpdate.class);
intent.putExtra(MagiskManager.INTENT_LINK, mm.managerLink);
intent.putExtra(MagiskManager.INTENT_VERSION, mm.remoteManagerVersionString);
mm.sendBroadcast(intent);
})
.setNegativeButton(R.string.no_thanks, null)
.show();
}
public static void showUninstallDialog(MagiskFragment fragment) {
MagiskManager mm = Utils.getMagiskManager(fragment.getActivity());
new AlertDialogBuilder(fragment.getActivity())
.setTitle(R.string.uninstall_magisk_title)
.setMessage(R.string.uninstall_magisk_msg)
.setPositiveButton(R.string.complete_uninstall, (d, i) -> {
try {
InputStream in = mm.getAssets().open(UNINSTALLER);
File uninstaller = new File(mm.getCacheDir(), UNINSTALLER);
FileOutputStream out = new FileOutputStream(uninstaller);
byte[] bytes = new byte[1024];
int read;
while ((read = in.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
in.close();
out.close();
in = mm.getAssets().open(UTIL_FUNCTIONS);
File utils = new File(mm.getCacheDir(), UTIL_FUNCTIONS);
out = new FileOutputStream(utils);
while ((read = in.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
in.close();
out.close();
Shell.getShell(mm).su(
"cat " + uninstaller + " > /cache/" + UNINSTALLER,
"cat " + utils + " > /data/magisk/" + UTIL_FUNCTIONS
);
mm.toast(R.string.uninstall_toast, Toast.LENGTH_LONG);
Shell.getShell(mm).su_raw(
"sleep 5",
"pm uninstall " + mm.getApplicationInfo().packageName
);
} catch (IOException e) {
e.printStackTrace();
}
})
.setNeutralButton(R.string.restore_stock_boot, (d, i) -> {
String boot = fragment.getSelectedBootImage();
if (boot == null) return;
new RestoreStockBoot(mm, boot).exec();
})
.setNegativeButton(R.string.no_thanks, null)
.show();
}
public static boolean useFDE(Context context) {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
&& context.getSystemService(DevicePolicyManager.class).getStorageEncryptionStatus()
== DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER;
}
public static Context getEncContext(Context context) {
if (useFDE(context))
return context.createDeviceProtectedStorageContext();
else
return context;
}
}

View File

@@ -1,117 +0,0 @@
package com.topjohnwu.magisk.utils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
public class WebRequest {
static String response = null;
public final static int GET = 1;
public final static int POST = 2;
//Constructor with no parameter
public WebRequest() {
}
/**
* Making web service call
*
* @url - url to make request
* @requestmethod - http request method
*/
public static String makeWebServiceCall(String url, int requestmethod) {
return makeWebServiceCall(url, requestmethod, null, false);
}
public static String makeWebServiceCall(String url, int requestmethod, boolean addNewLines) {
return makeWebServiceCall(url, requestmethod, null, addNewLines);
}
/**
* Making service call
*
* @url - url to make request
* @requestmethod - http request method
* @params - http request params
*/
public static String makeWebServiceCall(String urladdress, int requestmethod,
HashMap<String, String> params, boolean addNewLines) {
Logger.dev("WebRequest: Service call " + urladdress);
URL url;
String response = "";
try {
url = new URL(urladdress);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(15000);
conn.setConnectTimeout(15000);
conn.setDoInput(true);
if (requestmethod == POST) {
conn.setRequestMethod("POST");
} else if (requestmethod == GET) {
conn.setRequestMethod("GET");
}
if (params != null) {
OutputStream os = conn.getOutputStream();
BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(os, "UTF-8"));
StringBuilder result = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> entry : params.entrySet()) {
if (first)
first = false;
else
result.append("&");
result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
result.append("=");
result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
}
writer.write(result.toString());
writer.flush();
writer.close();
os.close();
}
int responseCode = conn.getResponseCode();
if (responseCode == HttpsURLConnection.HTTP_OK) {
String line;
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
while ((line = br.readLine()) != null) {
if (addNewLines) {
response += line + "\n";
} else {
response += line;
}
}
} else {
response = "";
}
} catch (Exception e) {
e.printStackTrace();
}
return response;
}
}

View File

@@ -0,0 +1,72 @@
package com.topjohnwu.magisk.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Map;
public class WebService {
public static String getString(String url) {
return getString(url, null);
}
public static String getString(String url, Map<String, String> header) {
HttpURLConnection conn = request(url, header);
if (conn == null) return "";
return getString(conn);
}
public static String getString(HttpURLConnection conn) {
try {
StringBuilder builder = new StringBuilder();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
int len;
char buf[] = new char[4096];
while ((len = br.read(buf)) != -1) {
builder.append(buf, 0, len);
}
}
conn.disconnect();
return builder.toString();
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
public static HttpURLConnection request(String address, Map<String, String> header) {
try {
URL url = new URL(address);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(15000);
conn.setConnectTimeout(15000);
if (header != null) {
for (Map.Entry<String, String> entry : header.entrySet()) {
conn.setRequestProperty(entry.getKey(), entry.getValue());
}
}
conn.connect();
if (header != null) {
header.clear();
for (Map.Entry<String, List<String>> entry : conn.getHeaderFields().entrySet()) {
List<String> l = entry.getValue();
header.put(entry.getKey(), l.get(l.size() - 1));
}
}
return conn;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

View File

@@ -1,41 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.preference.PreferenceManager;
import android.support.v7.app.AlertDialog;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.topjohnwu.magisk.R;
public class WebWindow {
public WebWindow(String title, String url, Context context) {
AlertDialog.Builder alert;
String theme = PreferenceManager.getDefaultSharedPreferences(context).getString("theme", "");
if (theme.equals("Dark")) {
alert = new AlertDialog.Builder(context, R.style.AlertDialog_dh);
} else {
alert = new AlertDialog.Builder(context);
}
alert.setTitle(title);
Logger.dev("WebView: URL = " + url);
WebView wv = new WebView(context);
wv.loadUrl(url);
wv.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
view.loadUrl(url);
return true;
}
});
alert.setView(wv);
alert.setNegativeButton("Close", (dialog, id) -> dialog.dismiss());
alert.show();
}
}

View File

@@ -1,747 +1,80 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.util.Pair;
import android.content.res.AssetManager;
import com.topjohnwu.magisk.utils.Utils.ByteArrayInOutStream;
import com.topjohnwu.jarsigner.JarMap;
import com.topjohnwu.jarsigner.SignAPK;
import org.spongycastle.asn1.ASN1InputStream;
import org.spongycastle.asn1.ASN1ObjectIdentifier;
import org.spongycastle.asn1.DEROutputStream;
import org.spongycastle.asn1.cms.CMSObjectIdentifiers;
import org.spongycastle.cert.jcajce.JcaCertStore;
import org.spongycastle.cms.CMSException;
import org.spongycastle.cms.CMSProcessableByteArray;
import org.spongycastle.cms.CMSSignedData;
import org.spongycastle.cms.CMSSignedDataGenerator;
import org.spongycastle.cms.CMSTypedData;
import org.spongycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
import org.spongycastle.jce.provider.BouncyCastleProvider;
import org.spongycastle.operator.ContentSigner;
import org.spongycastle.operator.OperatorCreationException;
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.spongycastle.util.encoders.Base64;
import java.io.ByteArrayOutputStream;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.security.DigestOutputStream;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.TreeMap;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
public class ZipUtils {
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
private static final String OTACERT_NAME = "META-INF/com/android/otacert";
// File name in assets
private static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem";
private static final String PRIVATE_KEY_NAME = "private.key.pk8";
private static Provider sBouncyCastleProvider;
// bitmasks for which hash algorithms we need the manifest to include.
private static final int USE_SHA1 = 1;
private static final int USE_SHA256 = 2;
static {
System.loadLibrary("zipadjust");
sBouncyCastleProvider = new BouncyCastleProvider();
Security.insertProviderAt(sBouncyCastleProvider, 1);
}
public native static byte[] zipAdjust(byte[] bytes, int size);
public native static void zipAdjust(String filenameIn, String filenameOut);
// Wrapper function for the JNI function
public static void adjustZip(ByteArrayInOutStream buffer) {
buffer.setBuffer(zipAdjust(buffer.toByteArray(), buffer.size()));
public static void unzip(File zip, File folder, String path, boolean junkPath) throws Exception {
InputStream in = new BufferedInputStream(new FileInputStream(zip));
unzip(in, folder, path, junkPath);
in.close();
}
public static void removeTopFolder(InputStream in, OutputStream out) {
public static void unzip(InputStream zip, File folder, String path, boolean junkPath) throws Exception {
byte data[] = new byte[4096];
try {
JarInputStream source = new JarInputStream(in);
JarOutputStream dest = new JarOutputStream(out);
JarInputStream zipfile = new JarInputStream(zip);
JarEntry entry;
String path;
int size;
byte buffer[] = new byte[4096];
while ((entry = source.getNextJarEntry()) != null) {
// Remove the top directory from the path
path = entry.getName().substring(entry.getName().indexOf("/") + 1);
// If it's the top folder, ignore it
if (path.isEmpty())
continue;
// Don't include placeholder
if (path.contains("system/placeholder"))
continue;
dest.putNextEntry(new JarEntry(path));
while((size = source.read(buffer, 0, 2048)) != -1)
dest.write(buffer, 0, size);
}
source.close();
dest.close();
in.close();
} catch (IOException e) {
e.printStackTrace();
Logger.dev("ZipUtils: removeTopFolder IO error!");
}
}
public static void unzip(File file, File folder) {
unzip(file, folder, "");
}
public static void unzip(File file, File folder, String path) {
try {
int count;
FileOutputStream out;
File dest;
InputStream is;
JarEntry entry;
byte data[] = new byte[4096];
JarFile zipfile = new JarFile(file);
Enumeration e = zipfile.entries();
while(e.hasMoreElements()) {
entry = (JarEntry) e.nextElement();
if (!entry.getName().contains(path)
|| entry.getName().charAt(entry.getName().length() - 1) == '/') {
while ((entry = zipfile.getNextJarEntry()) != null) {
if (!entry.getName().startsWith(path) || entry.isDirectory()){
// Ignore directories, only create files
continue;
}
Logger.dev("ZipUtils: Extracting: " + entry);
is = zipfile.getInputStream(entry);
dest = new File(folder, entry.getName());
if (dest.getParentFile().mkdirs()) {
dest.createNewFile();
String name;
if (junkPath) {
name = entry.getName().substring(entry.getName().lastIndexOf('/') + 1);
} else {
name = entry.getName();
}
out = new FileOutputStream(dest);
while ((count = is.read(data, 0, 4096)) != -1) {
File dest = new File(folder, name);
dest.getParentFile().mkdirs();
FileOutputStream out = new FileOutputStream(dest);
int count;
while ((count = zipfile.read(data)) != -1) {
out.write(data, 0, count);
}
out.flush();
out.close();
is.close();
}
} catch(Exception e) {
e.printStackTrace();
throw e;
}
}
public static void signZip(Context context, InputStream inputStream,
OutputStream outputStream, boolean signWholeFile) {
JarMap inputJar;
int hashes = 0;
try {
X509Certificate publicKey = readPublicKey(context.getAssets().open(PUBLIC_KEY_NAME));
hashes |= getDigestAlgorithm(publicKey);
// Set the ZIP file timestamp to the starting valid time
// of the 0th certificate plus one hour (to match what
// we've historically done).
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
PrivateKey privateKey = readPrivateKey(context.getAssets().open(PRIVATE_KEY_NAME));
inputJar = new JarMap(new JarInputStream(inputStream));
if (signWholeFile) {
if (!"RSA".equalsIgnoreCase(privateKey.getAlgorithm())) {
System.err.println("Cannot sign OTA packages with non-RSA keys");
System.exit(1);
}
signWholeFile(inputJar, context.getAssets().open(PUBLIC_KEY_NAME),
publicKey, privateKey, outputStream);
} else {
JarOutputStream outputJar = new JarOutputStream(outputStream);
// For signing .apks, use the maximum compression to make
// them as small as possible (since they live forever on
// the system partition). For OTA packages, use the
// default compression level, which is much much faster
// and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested).
outputJar.setLevel(9);
Manifest manifest = addDigestsToManifest(inputJar, hashes);
copyFiles(manifest, inputJar, outputJar, timestamp);
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
outputJar.close();
}
} catch (Exception e) {
e.printStackTrace();
}
public static void signZip(Context context, InputStream is, File output, boolean minSign) throws Exception {
signZip(context, new JarMap(is, false), output, minSign);
}
public static class JarMap extends TreeMap<String, Pair<JarEntry, ByteArrayOutputStream> > {
private Manifest manifest;
public JarMap(JarInputStream in) throws IOException {
super();
manifest = in.getManifest();
byte[] buffer = new byte[4096];
int num;
JarEntry entry;
while ((entry = in.getNextJarEntry()) != null) {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
while ((num = in.read(buffer)) > 0) {
stream.write(buffer, 0, num);
}
put(entry.getName(), entry, stream);
}
in.close();
}
public JarEntry getJarEntry(String name) {
return get(name).first;
}
public ByteArrayOutputStream getStream(String name) {
return get(name).second;
}
public void put(String name, JarEntry entry, ByteArrayOutputStream stream) {
put(name, new Pair<>(entry, stream));
}
public Manifest getManifest() {
return manifest;
}
public static void signZip(Context context, File input, File output, boolean minSign) throws Exception {
signZip(context, new JarMap(input, false), output, minSign);
}
/**
* Return one of USE_SHA1 or USE_SHA256 according to the signature
* algorithm specified in the cert.
*/
private static int getDigestAlgorithm(X509Certificate cert) {
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
if ("SHA1WITHRSA".equals(sigAlg) ||
"MD5WITHRSA".equals(sigAlg)) { // see "HISTORICAL NOTE" above.
return USE_SHA1;
} else if (sigAlg.startsWith("SHA256WITH")) {
return USE_SHA256;
} else {
throw new IllegalArgumentException("unsupported signature algorithm \"" + sigAlg +
"\" in cert [" + cert.getSubjectDN());
}
public static void signZip(Context context, JarMap input, File output, boolean minSign) throws Exception {
AssetManager assets = context.getAssets();
SignAPK.signZip(
assets.open(PUBLIC_KEY_NAME), assets.open(PRIVATE_KEY_NAME),
input, output, minSign);
}
/** Returns the expected signature algorithm for this key type. */
private static String getSignatureAlgorithm(X509Certificate cert) {
String sigAlg = cert.getSigAlgName().toUpperCase(Locale.US);
String keyType = cert.getPublicKey().getAlgorithm().toUpperCase(Locale.US);
if ("RSA".equalsIgnoreCase(keyType)) {
if (getDigestAlgorithm(cert) == USE_SHA256) {
return "SHA256withRSA";
} else {
return "SHA1withRSA";
}
} else if ("DSA".equalsIgnoreCase(keyType)) {
return "SHA256withDSA";
} else if ("EC".equalsIgnoreCase(keyType)) {
return "SHA256withECDSA";
} else {
throw new IllegalArgumentException("unsupported key type: " + keyType);
}
}
// Files matching this pattern are not copied to the output.
private static Pattern stripPattern =
Pattern.compile("^(META-INF/((.*)[.](SF|RSA|DSA|EC)|com/android/otacert))|(" +
Pattern.quote(JarFile.MANIFEST_NAME) + ")$");
private static X509Certificate readPublicKey(InputStream input)
throws IOException, GeneralSecurityException {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate) cf.generateCertificate(input);
} finally {
input.close();
}
}
/**
* Decrypt an encrypted PKCS 8 format private key.
*
* Based on ghstark's post on Aug 6, 2006 at
* http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
*
* @param encryptedPrivateKey The raw data of the private key
* @param keyFile The file containing the private key
*/
private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
throws GeneralSecurityException {
EncryptedPrivateKeyInfo epkInfo;
try {
epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
} catch (IOException ex) {
// Probably not an encrypted key.
return null;
}
// We no longer have console, so need to use another way to input password
// This function is left here if needed in the future, so no use for now
char[] password = new char[0];
SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
Key key = skFactory.generateSecret(new PBEKeySpec(password));
Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
try {
return epkInfo.getKeySpec(cipher);
} catch (InvalidKeySpecException ex) {
System.err.println("signapk: Password for " + keyFile + " may be bad.");
throw ex;
}
}
/** Read a PKCS 8 format private key. */
private static PrivateKey readPrivateKey(InputStream input)
throws IOException, GeneralSecurityException {
try {
byte[] buffer = new byte[4096];
int size = input.read(buffer);
byte[] bytes = Arrays.copyOf(buffer, size);
KeySpec spec = new PKCS8EncodedKeySpec(bytes);
PrivateKey key;
key = decodeAsKeyType(spec, "RSA");
if (key != null) {
return key;
}
key = decodeAsKeyType(spec, "DSA");
if (key != null) {
return key;
}
key = decodeAsKeyType(spec, "EC");
if (key != null) {
return key;
}
throw new NoSuchAlgorithmException("Must be an RSA, DSA, or EC key");
} finally {
input.close();
}
}
private static PrivateKey decodeAsKeyType(KeySpec spec, String keyType)
throws GeneralSecurityException {
try {
return KeyFactory.getInstance(keyType).generatePrivate(spec);
} catch (InvalidKeySpecException e) {
return null;
}
}
/**
* Add the hash(es) of every file to the manifest, creating it if
* necessary.
*/
private static Manifest addDigestsToManifest(JarMap jar, int hashes)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
if (input != null) {
main.putAll(input.getMainAttributes());
} else {
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
}
MessageDigest md_sha1 = null;
MessageDigest md_sha256 = null;
if ((hashes & USE_SHA1) != 0) {
md_sha1 = MessageDigest.getInstance("SHA1");
}
if ((hashes & USE_SHA256) != 0) {
md_sha256 = MessageDigest.getInstance("SHA256");
}
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
/* JarMap is a TreeMap, so it's already sorted */
for (String name : jar.keySet()) {
JarEntry entry = jar.getJarEntry(name);
if (!entry.isDirectory() &&
(stripPattern == null || !stripPattern.matcher(name).matches())) {
byte[] buffer = jar.getStream(name).toByteArray();
if (md_sha1 != null) md_sha1.update(buffer, 0, buffer.length);
if (md_sha256 != null) md_sha256.update(buffer, 0, buffer.length);
Attributes attr = null;
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
if (md_sha1 != null) {
attr.putValue("SHA1-Digest",
new String(Base64.encode(md_sha1.digest()), "ASCII"));
}
if (md_sha256 != null) {
attr.putValue("SHA-256-Digest",
new String(Base64.encode(md_sha256.digest()), "ASCII"));
}
output.getEntries().put(name, attr);
}
}
return output;
}
/**
* Add a copy of the public key to the archive; this should
* exactly match one of the files in
* /system/etc/security/otacerts.zip on the device. (The same
* cert can be extracted from the CERT.RSA file but this is much
* easier to get at.)
*/
private static void addOtacert(JarOutputStream outputJar,
InputStream input,
long timestamp,
Manifest manifest,
int hash)
throws IOException, GeneralSecurityException {
MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
JarEntry je = new JarEntry(OTACERT_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
byte[] b = new byte[4096];
int read;
while ((read = input.read(b)) != -1) {
outputJar.write(b, 0, read);
md.update(b, 0, read);
}
input.close();
Attributes attr = new Attributes();
attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
new String(Base64.encode(md.digest()), "ASCII"));
manifest.getEntries().put(OTACERT_NAME, attr);
}
/** Write to another stream and track how many bytes have been
* written.
*/
private static class CountOutputStream extends FilterOutputStream {
private int mCount;
public CountOutputStream(OutputStream out) {
super(out);
mCount = 0;
}
@Override
public void write(int b) throws IOException {
super.write(b);
mCount++;
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
super.write(b, off, len);
mCount += len;
}
public int size() {
return mCount;
}
}
/** Write a .SF file with a digest of the specified manifest. */
private static void writeSignatureFile(Manifest manifest, OutputStream out,
int hash)
throws IOException, GeneralSecurityException {
Manifest sf = new Manifest();
Attributes main = sf.getMainAttributes();
main.putValue("Signature-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
MessageDigest md = MessageDigest.getInstance(
hash == USE_SHA256 ? "SHA256" : "SHA1");
PrintStream print = new PrintStream(
new DigestOutputStream(new ByteArrayOutputStream(), md),
true, "UTF-8");
// Digest of the entire manifest
manifest.write(print);
print.flush();
main.putValue(hash == USE_SHA256 ? "SHA-256-Digest-Manifest" : "SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
Map<String, Attributes> entries = manifest.getEntries();
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
// Digest of the manifest stanza for this entry.
print.print("Name: " + entry.getKey() + "\r\n");
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
}
print.print("\r\n");
print.flush();
Attributes sfAttr = new Attributes();
sfAttr.putValue(hash == USE_SHA256 ? "SHA-256-Digest" : "SHA1-Digest-Manifest",
new String(Base64.encode(md.digest()), "ASCII"));
sf.getEntries().put(entry.getKey(), sfAttr);
}
CountOutputStream cout = new CountOutputStream(out);
sf.write(cout);
// A bug in the java.util.jar implementation of Android platforms
// up to version 1.6 will cause a spurious IOException to be thrown
// if the length of the signature file is a multiple of 1024 bytes.
// As a workaround, add an extra CRLF in this case.
if ((cout.size() % 1024) == 0) {
cout.write('\r');
cout.write('\n');
}
}
/** Sign data and write the digital signature to 'out'. */
private static void writeSignatureBlock(
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)
throws IOException, CertificateEncodingException, OperatorCreationException, CMSException {
ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
certList.add(publicKey);
JcaCertStore certs = new JcaCertStore(certList);
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
ContentSigner signer = new JcaContentSignerBuilder(getSignatureAlgorithm(publicKey))
.setProvider(sBouncyCastleProvider)
.build(privateKey);
gen.addSignerInfoGenerator(
new JcaSignerInfoGeneratorBuilder(
new JcaDigestCalculatorProviderBuilder()
.setProvider(sBouncyCastleProvider)
.build())
.setDirectSignature(true)
.build(signer, publicKey));
gen.addCertificates(certs);
CMSSignedData sigData = gen.generate(data, false);
ASN1InputStream asn1 = new ASN1InputStream(sigData.getEncoded());
DEROutputStream dos = new DEROutputStream(out);
dos.writeObject(asn1.readObject());
}
/**
* Copy all the files in a manifest from input to output. We set
* the modification times in the output to a fixed time, so as to
* reduce variation in the output file and make incremental OTAs
* more efficient.
*/
private static void copyFiles(Manifest manifest,
JarMap in, JarOutputStream out, long timestamp) throws IOException {
Map<String, Attributes> entries = manifest.getEntries();
ArrayList<String> names = new ArrayList<>(entries.keySet());
Collections.sort(names);
for (String name : names) {
JarEntry inEntry = in.getJarEntry(name);
JarEntry outEntry;
if (inEntry.getMethod() == JarEntry.STORED) {
// Preserve the STORED method of the input entry.
outEntry = new JarEntry(inEntry);
} else {
// Create a new entry so that the compressed len is recomputed.
outEntry = new JarEntry(name);
}
outEntry.setTime(timestamp);
out.putNextEntry(outEntry);
in.getStream(name).writeTo(out);
out.flush();
}
}
private static class WholeFileSignerOutputStream extends FilterOutputStream {
private boolean closing = false;
private ByteArrayOutputStream footer = new ByteArrayOutputStream();
private OutputStream tee;
public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
super(out);
this.tee = tee;
}
public void notifyClosing() {
closing = true;
}
public void finish() throws IOException {
closing = false;
byte[] data = footer.toByteArray();
if (data.length < 2)
throw new IOException("Less than two bytes written to footer");
write(data, 0, data.length - 2);
}
public byte[] getTail() {
return footer.toByteArray();
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (closing) {
// if the jar is about to close, save the footer that will be written
footer.write(b, off, len);
}
else {
// write to both output streams. out is the CMSTypedData signer and tee is the file.
out.write(b, off, len);
tee.write(b, off, len);
}
}
@Override
public void write(int b) throws IOException {
if (closing) {
// if the jar is about to close, save the footer that will be written
footer.write(b);
}
else {
// write to both output streams. out is the CMSTypedData signer and tee is the file.
out.write(b);
tee.write(b);
}
}
}
private static class CMSSigner implements CMSTypedData {
private JarMap inputJar;
private InputStream publicKeyFile;
private X509Certificate publicKey;
private PrivateKey privateKey;
private OutputStream outputStream;
private final ASN1ObjectIdentifier type;
private WholeFileSignerOutputStream signer;
public CMSSigner(JarMap inputJar, InputStream publicKeyFile,
X509Certificate publicKey, PrivateKey privateKey,
OutputStream outputStream) {
this.inputJar = inputJar;
this.publicKeyFile = publicKeyFile;
this.publicKey = publicKey;
this.privateKey = privateKey;
this.outputStream = outputStream;
this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
}
public Object getContent() {
// Not supported, but still don't crash or return null
return 1;
}
public ASN1ObjectIdentifier getContentType() {
return type;
}
public void write(OutputStream out) throws IOException {
try {
signer = new WholeFileSignerOutputStream(out, outputStream);
JarOutputStream outputJar = new JarOutputStream(signer);
int hash = getDigestAlgorithm(publicKey);
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
Manifest manifest = addDigestsToManifest(inputJar, hash);
copyFiles(manifest, inputJar, outputJar, timestamp);
// Don't add Otacert, it's not an OTA
// addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
signer.notifyClosing();
outputJar.close();
signer.finish();
}
catch (Exception e) {
throw new IOException(e);
}
}
public void writeSignatureBlock(ByteArrayOutputStream temp)
throws IOException,
CertificateEncodingException,
OperatorCreationException,
CMSException {
ZipUtils.writeSignatureBlock(this, publicKey, privateKey, temp);
}
public WholeFileSignerOutputStream getSigner() {
return signer;
}
}
private static void signWholeFile(JarMap inputJar, InputStream publicKeyFile,
X509Certificate publicKey, PrivateKey privateKey,
OutputStream outputStream) throws Exception {
CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
publicKey, privateKey, outputStream);
ByteArrayOutputStream temp = new ByteArrayOutputStream();
// put a readable message and a null char at the start of the
// archive comment, so that tools that display the comment
// (hopefully) show something sensible.
// TODO: anything more useful we can put in this message?
byte[] message = "signed by SignApk".getBytes("UTF-8");
temp.write(message);
temp.write(0);
cmsOut.writeSignatureBlock(temp);
byte[] zipData = cmsOut.getSigner().getTail();
// For a zip with no archive comment, the
// end-of-central-directory record will be 22 bytes long, so
// we expect to find the EOCD marker 22 bytes from the end.
if (zipData[zipData.length-22] != 0x50 ||
zipData[zipData.length-21] != 0x4b ||
zipData[zipData.length-20] != 0x05 ||
zipData[zipData.length-19] != 0x06) {
throw new IllegalArgumentException("zip data already has an archive comment");
}
int total_size = temp.size() + 6;
if (total_size > 0xffff) {
throw new IllegalArgumentException("signature is too big for ZIP file comment");
}
// signature starts this many bytes from the end of the file
int signature_start = total_size - message.length - 1;
temp.write(signature_start & 0xff);
temp.write((signature_start >> 8) & 0xff);
// Why the 0xff bytes? In a zip file with no archive comment,
// bytes [-6:-2] of the file are the little-endian offset from
// the start of the file to the central directory. So for the
// two high bytes to be 0xff 0xff, the archive would have to
// be nearly 4GB in size. So it's unlikely that a real
// commentless archive would have 0xffs here, and lets us tell
// an old signed archive from a new one.
temp.write(0xff);
temp.write(0xff);
temp.write(total_size & 0xff);
temp.write((total_size >> 8) & 0xff);
temp.flush();
// Signature verification checks that the EOCD header is the
// last such sequence in the file (to avoid minzip finding a
// fake EOCD appended after the signature in its scan). The
// odds of producing this sequence by chance are very low, but
// let's catch it here if it does.
byte[] b = temp.toByteArray();
for (int i = 0; i < b.length-3; ++i) {
if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) {
throw new IllegalArgumentException("found spurious EOCD header at " + i);
}
}
outputStream.write(total_size & 0xff);
outputStream.write((total_size >> 8) & 0xff);
temp.writeTo(outputStream);
}
private static void signFile(Manifest manifest, JarMap inputJar,
X509Certificate publicKey, PrivateKey privateKey,
JarOutputStream outputJar)
throws Exception {
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
// MANIFEST.MF
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
// CERT.SF / CERT#.SF
je = new JarEntry(CERT_SF_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
// CERT.{DSA,EC,RSA} / CERT#.{DSA,EC,RSA}
je = new JarEntry((String.format(CERT_SIG_NAME, privateKey.getAlgorithm())));
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(new CMSProcessableByteArray(signedData),
publicKey, privateKey, outputJar);
}
}
}

View File

@@ -0,0 +1,7 @@
cmake_minimum_required(VERSION 3.6)
add_library(zipadjust SHARED
jni_glue.c
zipadjust.c)
find_library(libz z)
find_library(liblog log)
target_link_libraries(zipadjust ${libz} ${liblog})

View File

@@ -0,0 +1,19 @@
//
// Java entry point
//
#include <jni.h>
#include "zipadjust.h"
JNIEXPORT void JNICALL
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust(JNIEnv *env, jclass type, jstring filenameIn_,
jstring filenameOut_) {
const char *filenameIn = (*env)->GetStringUTFChars(env, filenameIn_, 0);
const char *filenameOut = (*env)->GetStringUTFChars(env, filenameOut_, 0);
// TODO
zipadjust(filenameIn, filenameOut, 0);
(*env)->ReleaseStringUTFChars(env, filenameIn_, filenameIn);
(*env)->ReleaseStringUTFChars(env, filenameOut_, filenameOut);
}

View File

@@ -1,35 +1,14 @@
#include <jni.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <zlib.h>
#include <android/log.h>
#include <unistd.h>
#include "zipadjust.h"
#define LOG_TAG "zipadjust"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
size_t insize, outsize = 0, alloc = 0;
unsigned char *fin, *fout;
int zipadjust(int decompress) ;
JNIEXPORT jbyteArray JNICALL
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust(JNIEnv *env, jclass type, jbyteArray jbytes,
jint size) {
fin = (*env)->GetPrimitiveArrayCritical(env, jbytes, NULL);
insize = (size_t) size;
zipadjust(0);
(*env)->ReleasePrimitiveArrayCritical(env, jbytes, fin, 0);
jbyteArray ret = (*env)->NewByteArray(env, outsize);
(*env)->SetByteArrayRegion(env, ret, 0, outsize, (const jbyte*) fout);
free(fout);
return ret;
}
#ifndef O_BINARY
#define O_BINARY 0
#define O_TEXT 0
#endif
#pragma pack(1)
struct local_header_struct {
@@ -107,41 +86,43 @@ static int xerror(char* message) {
return 0;
}
static int xseekread(off_t offset, void* buf, size_t bytes) {
memcpy(buf, fin + offset, bytes);
static int xseekread(int fd, off_t offset, void* buf, size_t bytes) {
if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return xerror("Seek failed");
if (read(fd, buf, bytes) != bytes) return xerror("Read failed");
return 1;
}
static int xseekwrite(off_t offset, const void* buf, size_t bytes) {
if (offset + bytes > outsize) outsize = offset + bytes;
if (outsize > alloc) {
fout = realloc(fout, outsize);
alloc = outsize;
}
memcpy(fout + offset, buf, bytes);
static int xseekwrite(int fd, off_t offset, void* buf, size_t bytes) {
if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return xerror("Seek failed");
if (write(fd, buf, bytes) != bytes) return xerror("Write failed");
return 1;
}
static int xfilecopy(off_t offsetIn, off_t offsetOut, size_t bytes) {
unsigned int CHUNK = 256 * 1024;
unsigned char* buf = malloc(CHUNK);
static int xfilecopy(int fdIn, int fdOut, off_t offsetIn, off_t offsetOut, size_t bytes) {
if ((offsetIn != (off_t)-1) && (lseek(fdIn, offsetIn, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
if ((offsetOut != (off_t)-1) && (lseek(fdOut, offsetOut, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
int CHUNK = 256 * 1024;
void* buf = malloc(CHUNK);
if (buf == NULL) return xerror("malloc failed");
size_t left = bytes;
while (left > 0) {
size_t wanted = (left < CHUNK) ? left : CHUNK;
xseekread(offsetIn, buf, wanted);
xseekwrite(offsetOut, buf, wanted);
offsetIn += wanted;
offsetOut += wanted;
left -= wanted;
size_t r = read(fdIn, buf, wanted);
if (r <= 0) return xerror("Read failed");
if (write(fdOut, buf, r) != r) return xerror("Write failed");
left -= r;
}
free(buf);
return 1;
}
static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
unsigned int CHUNK = 256 * 1024;
static int xdecompress(int fdIn, int fdOut, off_t offsetIn, off_t offsetOut, size_t bytes) {
if ((offsetIn != (off_t)-1) && (lseek(fdIn, offsetIn, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
if ((offsetOut != (off_t)-1) && (lseek(fdOut, offsetOut, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
int CHUNK = 256 * 1024;
int ret;
unsigned have;
@@ -158,12 +139,9 @@ static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
if (ret != Z_OK) return xerror("ret != Z_OK");
do {
strm.avail_in = insize - offsetIn;
strm.avail_in = read(fdIn, in, CHUNK);
if (strm.avail_in == 0) break;
strm.avail_in = (strm.avail_in > CHUNK) ? CHUNK : strm.avail_in;
xseekread(offsetIn, in, strm.avail_in);
strm.next_in = in;
offsetIn += strm.avail_in;
do {
strm.avail_out = CHUNK;
@@ -181,8 +159,10 @@ static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
}
have = CHUNK - strm.avail_out;
xseekwrite(offsetOut, out, have);
offsetOut += have;
if (write(fdOut, out, have) != have) {
(void)inflateEnd(&strm);
return xerror("Write failed");
}
} while (strm.avail_out == 0);
} while (ret != Z_STREAM_END);
(void)inflateEnd(&strm);
@@ -190,118 +170,128 @@ static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
return ret == Z_STREAM_END ? 1 : 0;
}
int zipadjust(int decompress) {
int zipadjust(const char* filenameIn, const char* filenameOut, int decompress) {
int ok = 0;
char filename[1024];
int fin = open(filenameIn, O_RDONLY | O_BINARY);
if (fin > 0) {
unsigned int size = lseek(fin, 0, SEEK_END);
lseek(fin, 0, SEEK_SET);
LOGD("%d bytes\n", size);
central_footer_t central_footer;
uint32_t central_directory_in_position = 0;
uint32_t central_directory_in_size = 0;
uint32_t central_directory_out_size = 0;
char filename[1024];
int i;
for (i = insize - 4; i >= 0; i--) {
uint32_t magic = 0;
if (!xseekread(i, &magic, sizeof(uint32_t))) return 0;
if (magic == MAGIC_CENTRAL_FOOTER) {
LOGD("central footer @ %08X\n", i);
if (!xseekread(i, &central_footer, sizeof(central_footer_t))) return 0;
central_footer_t central_footer;
uint32_t central_directory_in_position = 0;
uint32_t central_directory_in_size = 0;
uint32_t central_directory_out_size = 0;
central_header_t central_header;
if (!xseekread(central_footer.central_directory_offset, &central_header, sizeof(central_header_t))) return 0;
if ( central_header.signature == MAGIC_CENTRAL_HEADER ) {
central_directory_in_position = central_footer.central_directory_offset;
central_directory_in_size = insize - central_footer.central_directory_offset;
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
break;
int i;
for (i = size - 4; i >= 0; i--) {
uint32_t magic = 0;
if (!xseekread(fin, i, &magic, sizeof(uint32_t))) return 0;
if (magic == MAGIC_CENTRAL_FOOTER) {
LOGD("central footer @ %08X\n", i);
if (!xseekread(fin, i, &central_footer, sizeof(central_footer_t))) return 0;
central_header_t central_header;
if (!xseekread(fin, central_footer.central_directory_offset, &central_header, sizeof(central_header_t))) return 0;
if ( central_header.signature == MAGIC_CENTRAL_HEADER ) {
central_directory_in_position = central_footer.central_directory_offset;
central_directory_in_size = size - central_footer.central_directory_offset;
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
break;
}
}
}
if (central_directory_in_position == 0) return 0;
unsigned char* central_directory_in = (unsigned char*)malloc(central_directory_in_size);
unsigned char* central_directory_out = (unsigned char*)malloc(central_directory_in_size);
if (!xseekread(fin, central_directory_in_position, central_directory_in, central_directory_in_size)) return 0;
memset(central_directory_out, 0, central_directory_in_size);
unlink(filenameOut);
int fout = open(filenameOut, O_CREAT | O_WRONLY | O_BINARY, 0644);
if (fout > 0) {
uintptr_t central_directory_in_index = 0;
uintptr_t central_directory_out_index = 0;
central_header_t* central_header = NULL;
uint32_t out_index = 0;
while (1) {
central_header = (central_header_t*)&central_directory_in[central_directory_in_index];
if (central_header->signature != MAGIC_CENTRAL_HEADER) break;
filename[central_header->length_filename] = (char)0;
memcpy(filename, &central_directory_in[central_directory_in_index + sizeof(central_header_t)], central_header->length_filename);
LOGD("%s (%d --> %d) [%08X] (%d)\n", filename, central_header->size_uncompressed, central_header->size_compressed, central_header->crc32, central_header->length_extra + central_header->length_comment);
local_header_t local_header;
if (!xseekread(fin, central_header->offset, &local_header, sizeof(local_header_t))) return 0;
// save and update to next index before we clobber the data
uint16_t compression_method_old = central_header->compression_method;
uint32_t size_compressed_old = central_header->size_compressed;
uint32_t offset_old = central_header->offset;
uint32_t length_extra_old = central_header->length_extra;
central_directory_in_index += sizeof(central_header_t) + central_header->length_filename + central_header->length_extra + central_header->length_comment;
// copying, rewriting, and correcting local and central headers so all the information matches, and no data descriptors are necessary
central_header->offset = out_index;
central_header->flags = central_header->flags & !8;
if (decompress && (compression_method_old == 8)) {
central_header->compression_method = 0;
central_header->size_compressed = central_header->size_uncompressed;
}
central_header->length_extra = 0;
central_header->length_comment = 0;
local_header.compression_method = central_header->compression_method;
local_header.flags = central_header->flags;
local_header.crc32 = central_header->crc32;
local_header.size_uncompressed = central_header->size_uncompressed;
local_header.size_compressed = central_header->size_compressed;
local_header.length_extra = 0;
if (!xseekwrite(fout, out_index, &local_header, sizeof(local_header_t))) return 0;
out_index += sizeof(local_header_t);
if (!xseekwrite(fout, out_index, &filename[0], central_header->length_filename)) return 0;
out_index += central_header->length_filename;
if (decompress && (compression_method_old == 8)) {
if (!xdecompress(fin, fout, offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
} else {
if (!xfilecopy(fin, fout, offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
}
out_index += local_header.size_compressed;
memcpy(&central_directory_out[central_directory_out_index], central_header, sizeof(central_header_t) + central_header->length_filename);
central_directory_out_index += sizeof(central_header_t) + central_header->length_filename;
}
central_directory_out_size = central_directory_out_index;
central_footer.central_directory_size = central_directory_out_size;
central_footer.central_directory_offset = out_index;
central_footer.length_comment = 0;
if (!xseekwrite(fout, out_index, central_directory_out, central_directory_out_size)) return 0;
out_index += central_directory_out_size;
if (!xseekwrite(fout, out_index, &central_footer, sizeof(central_footer_t))) return 0;
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
LOGD("central footer @ %08X\n", out_index);
close(fout);
ok = 1;
}
free(central_directory_in);
free(central_directory_out);
close(fin);
}
if (central_directory_in_position == 0) return 0;
unsigned char* central_directory_in = (unsigned char*)malloc(central_directory_in_size);
unsigned char* central_directory_out = (unsigned char*)malloc(central_directory_in_size);
if (!xseekread(central_directory_in_position, central_directory_in, central_directory_in_size)) return 0;
memset(central_directory_out, 0, central_directory_in_size);
fout = (unsigned char*) malloc(insize);
alloc = insize;
uintptr_t central_directory_in_index = 0;
uintptr_t central_directory_out_index = 0;
central_header_t* central_header = NULL;
uint32_t out_index = 0;
while (1) {
central_header = (central_header_t*)&central_directory_in[central_directory_in_index];
if (central_header->signature != MAGIC_CENTRAL_HEADER) break;
filename[central_header->length_filename] = (char)0;
memcpy(filename, &central_directory_in[central_directory_in_index + sizeof(central_header_t)], central_header->length_filename);
LOGD("%s (%d --> %d) [%08X] (%d)\n", filename, central_header->size_uncompressed, central_header->size_compressed, central_header->crc32, central_header->length_extra + central_header->length_comment);
local_header_t local_header;
if (!xseekread(central_header->offset, &local_header, sizeof(local_header_t))) return 0;
// save and update to next index before we clobber the data
uint16_t compression_method_old = central_header->compression_method;
uint32_t size_compressed_old = central_header->size_compressed;
uint32_t offset_old = central_header->offset;
uint32_t length_extra_old = central_header->length_extra;
central_directory_in_index += sizeof(central_header_t) + central_header->length_filename + central_header->length_extra + central_header->length_comment;
// copying, rewriting, and correcting local and central headers so all the information matches, and no data descriptors are necessary
central_header->offset = out_index;
central_header->flags = central_header->flags & !8;
if (decompress && (compression_method_old == 8)) {
central_header->compression_method = 0;
central_header->size_compressed = central_header->size_uncompressed;
}
central_header->length_extra = 0;
central_header->length_comment = 0;
local_header.compression_method = central_header->compression_method;
local_header.flags = central_header->flags;
local_header.crc32 = central_header->crc32;
local_header.size_uncompressed = central_header->size_uncompressed;
local_header.size_compressed = central_header->size_compressed;
local_header.length_extra = 0;
if (!xseekwrite(out_index, &local_header, sizeof(local_header_t))) return 0;
out_index += sizeof(local_header_t);
if (!xseekwrite(out_index, &filename[0], central_header->length_filename)) return 0;
out_index += central_header->length_filename;
if (decompress && (compression_method_old == 8)) {
if (!xdecompress(offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
} else {
if (!xfilecopy(offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
}
out_index += local_header.size_compressed;
memcpy(&central_directory_out[central_directory_out_index], central_header, sizeof(central_header_t) + central_header->length_filename);
central_directory_out_index += sizeof(central_header_t) + central_header->length_filename;
}
central_directory_out_size = central_directory_out_index;
central_footer.central_directory_size = central_directory_out_size;
central_footer.central_directory_offset = out_index;
central_footer.length_comment = 0;
if (!xseekwrite(out_index, central_directory_out, central_directory_out_size)) return 0;
out_index += central_directory_out_size;
if (!xseekwrite(out_index, &central_footer, sizeof(central_footer_t))) return 0;
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
LOGD("central footer @ %08X\n", out_index);
ok = 1;
free(central_directory_in);
free(central_directory_out);
return ok;
}
}

View File

@@ -0,0 +1,13 @@
#ifndef MAGISKMANAGER_ZIPADJUST_H_H
#define MAGISKMANAGER_ZIPADJUST_H_H
#include <android/log.h>
int zipadjust(const char* filenameIn, const char* filenameOut, int decompress);
#define LOG_TAG "zipadjust"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#endif //MAGISKMANAGER_ZIPADJUST_H_H

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#000000"
android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
<path
android:pathData="M0-0.75h24v24H0z" />
</vector>

View File

@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#000"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View File

@@ -4,6 +4,6 @@
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#757575"
android:fillColor="#000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#000"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>

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