Compare commits

...

155 Commits

Author SHA1 Message Date
topjohnwu
12bbc7fd6b Update v7.3.1 changelog 2019-06-17 22:15:38 -07:00
topjohnwu
bf9ac8252b Cleanup UpdateInfo 2019-06-16 16:47:30 -07:00
topjohnwu
4a3f5dc619 Cleanups 2019-06-16 14:35:51 -07:00
Viktor De Pasquale
ca156befbd Fixed mapping generic pairs to policy crashing when no policy is found
The policy (app) is now deleted when found invalid (uninstalled)
2019-06-16 16:50:08 -04:00
Viktor De Pasquale
4db41e2ac4 Added attempted fix for parsing data off default thread 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
982a43fce1 Moved diff computation of policy list to the background thread 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
dd76a74e1c Fixed fast scroll button crashing while scrolling to undefined position 2019-06-16 16:50:08 -04:00
Viktor De Pasquale
70cb52b2c7 Fixed fast scroll button being visible when log is empty 2019-06-16 16:50:08 -04:00
topjohnwu
5c7f69acaa Separate SAR and legacy implementation 2019-06-16 12:45:32 -07:00
topjohnwu
f1d9015e5f Move load kernel info out of class 2019-06-15 22:25:09 -07:00
topjohnwu
e8d900c58e Fix typo 2019-06-15 18:12:12 -07:00
topjohnwu
a6241ae912 Fix magiskboot unpack option parsing 2019-06-15 16:15:12 -07:00
topjohnwu
4a697ca2ec Add 7.3.0 changelog 2019-06-14 22:39:31 -07:00
topjohnwu
58bec7f2c9 Update dependencies 2019-06-14 22:39:31 -07:00
Wenlin Shen
213f84985c Update Chinese (Traditional) translate
Update wordings to improve readability.
2019-06-13 21:33:24 -07:00
Viktor De Pasquale
074b1f8c61 Added one-click scroll to the bottom 2019-06-12 16:08:02 +02:00
topjohnwu
326eee8c83 Migrate a lot of classes to Kotlin 2019-06-12 03:29:38 -07:00
topjohnwu
00bff4912e Use svc for reboot if feasible
Close #1488
2019-06-12 00:55:21 -07:00
Viktor De Pasquale
0ce1720516 Fixed magisk log screen lines having multiple lines 2019-06-11 21:52:03 -07:00
osm0sis
ee407472cf magiskboot: allow forcing no recompression on ramdisk.cpio
- when input image had a compressed ramdisk magiskboot had no way to force the repack with the uncompressed ramdisk.cpio since it does not formally recognize cpio as its own format, so add a switch to support forcing repacking to any possible ramdisk format regardless of input image
2019-06-10 21:57:39 -07:00
osm0sis
f341f3b2dd magiskboot: accept forcing recognized but unsupported format compression
- when input image had a different supported format (e.g. gzip) magiskboot would not accept a manually compressed ramdisk or kernel in an unsupported format (e.g. lzop) despite being able to recognize it, so instead would double compress using whatever the input format was, breaking the image with, in effect, a ramdisk.cpio.lzo.gz
2019-06-10 21:56:51 -07:00
Ian Macdonald
8513946e09 'magiskboot hexpatch' will exit with status 1 when nothing patched. 2019-06-10 21:41:40 -07:00
nonnymoose
8ebd9c8927 Use original file type when creating device nodes 2019-06-10 21:41:17 -07:00
topjohnwu
1d54c5144e Fix background update checks 2019-06-10 21:25:42 -07:00
topjohnwu
e40d4318fa Let Kotlin target Java 8 2019-06-10 21:22:07 -07:00
topjohnwu
7756e10779 Rewrite configs with Kotlin delagate properties 2019-06-10 04:37:56 -07:00
topjohnwu
3e58d502d0 Update SettingsFragment to Kotlin 2019-06-09 03:04:37 -07:00
topjohnwu
1c8846dc57 Make PreferenceModel an interface 2019-06-08 16:30:12 -07:00
topjohnwu
2f320c7239 Update ClassMap 2019-06-08 15:34:15 -07:00
topjohnwu
e799918ab6 Update update check service 2019-06-08 15:28:59 -07:00
topjohnwu
86c4928e0f Fix locale settings
AppCompatActivity changed its impl again...
2019-06-08 02:11:10 -07:00
topjohnwu
0293eb5c51 Never refetch magisk version dynamically 2019-06-08 01:44:02 -07:00
topjohnwu
1ee75b6aa6 Download snet package without legacy impl 2019-06-08 01:39:22 -07:00
topjohnwu
4b30b224b5 Remove separate constant class 2019-06-08 00:41:03 -07:00
topjohnwu
16b232d2a3 Enable okhttp logging in debug only 2019-06-07 02:03:17 -07:00
topjohnwu
3f3b1f5b1d Sort policy with app name 2019-06-07 01:24:54 -07:00
topjohnwu
cec017b7bf More MagidkDB fixes 2019-06-07 01:05:54 -07:00
topjohnwu
3123cc1059 Update AndroidX dependencies 2019-06-07 00:27:07 -07:00
topjohnwu
caa9df86bc Switch to R8 friendly room-runtime 2019-06-07 00:17:00 -07:00
topjohnwu
f417389a7a Fix magisk database code in app 2019-06-06 00:39:24 -07:00
topjohnwu
662a5c8ea6 Upgrade Retrofit 2.6.0 2019-06-05 23:41:51 -07:00
topjohnwu
7edfbfb764 Upgrade SDK 2019-06-05 21:33:09 -07:00
topjohnwu
c1602d2554 Support execute commands in magiskhide env
Credits to #1454
2019-06-04 22:27:19 -07:00
topjohnwu
9f8d4e1022 Properly isolate mount namespace 2019-06-04 21:21:27 -07:00
Viktor De Pasquale
d1dfda405f Removed Kotpref and replaced it with PreferenceModel 2019-06-04 00:59:57 -07:00
Emanuel Hajnzic
28efded624 Update and cleanup for german strings.xml 2019-06-03 23:37:57 -07:00
topjohnwu
06c86ee267 Remove samsung.md 2019-06-03 23:37:16 -07:00
Ian Macdonald
5892780871 Added warnings about flashing only an AP file and using MTP.
MTP is now known to sometimes corrupt the AP file on transfer to the PC,
so we should warn users to prefer `adb`.

Furthermore, quite a few users are reporting a shrunken `/data`
file-system after flashing with Odin. This has been traced to the
flashing of only an AP file, which causes some versions of Odin to
shrink `/data`. The phenomenon is reproducable.
2019-06-03 23:35:33 -07:00
topjohnwu
4fcdcd9a8a Detect UID from data directories 2019-06-03 23:32:49 -07:00
topjohnwu
80d834fb55 Use kotshi instead of moshi-kotlin-codegen 2019-06-01 13:18:11 -07:00
topjohnwu
4122ebe18f Remove unused Room database code 2019-06-01 02:20:40 -07:00
topjohnwu
7d87777bf8 Improve proguard rules 2019-06-01 01:13:29 -07:00
topjohnwu
4a73d634e0 Tidy things up 2019-05-31 21:46:59 -07:00
topjohnwu
373dc10a40 Use moshi code-gen 2019-05-31 21:46:42 -07:00
Ian Macdonald
ed43ec8ea2 Populate Config variables based on update channel parameters.
With thanks to @diareuse.
2019-05-31 20:48:21 -07:00
topjohnwu
7918fc3528 Support building individual applets 2019-05-30 21:17:58 -07:00
osm0sis
bf58205b0a magiskboot: be clear lzop is not a supported compression format
- keep detection and always display detected format type to fascilitate external support
2019-05-30 20:31:24 -07:00
topjohnwu
c0d1ce96d1 Cleanup 2019-05-30 01:05:48 -07:00
topjohnwu
b31d3802eb Properly force refresh 2019-05-29 23:45:18 -07:00
Viktor De Pasquale
be1228c3b4 Reverted removing UpdateRepos temporarily 2019-05-29 18:40:16 +02:00
Viktor De Pasquale
15c94c6b34 Merge remote-tracking branch 'john/master' into development
# Conflicts:
#	build.gradle
2019-05-29 18:28:50 +02:00
Viktor De Pasquale
202d23426a Fixed update cards having their text resized 2019-05-29 16:35:02 +02:00
Viktor De Pasquale
fc26de48b2 Removed hiding advanced settings when no root is detected
This change was made in order to allow proper adjustment of boot image
2019-05-29 16:28:33 +02:00
vvb2060
76c88913f9 Ensure Magisk environment normal 2019-05-27 16:29:54 -07:00
topjohnwu
a3a1aed723 Don't check zygote in busy loop 2019-05-27 16:27:19 -07:00
topjohnwu
81aa56f60f Support EROFS system-as-root devices
Close #1381
2019-05-27 15:19:28 -07:00
Vladimír Kubala
73bb850209 Update Slovak translation 2019-05-27 15:04:30 -07:00
Gozzwip
8dfec12330 Some fixes
There is a missing string which I couldn't find in this file but in app it appears when you install a module, please check.
2019-05-27 15:04:12 -07:00
topjohnwu
ae24397793 Try to wait if block device is not ready
Close #1459
2019-05-27 15:01:49 -07:00
topjohnwu
3b0f888407 Minor update for parsing uevent 2019-05-27 02:55:46 -07:00
topjohnwu
845d1e02b0 Separate magiskinit components 2019-05-27 00:29:43 -07:00
topjohnwu
5d357bc41f Remove unused function 2019-05-26 22:01:42 -07:00
topjohnwu
6a54672b13 Cleanup unnecessary functions 2019-05-26 03:05:23 -07:00
topjohnwu
3d9a15df44 Remove unnecessary '--' in magiskhide 2019-05-26 02:59:38 -07:00
topjohnwu
449c7fda2f Enable proc_monitor test in debug mode only 2019-05-26 02:53:28 -07:00
topjohnwu
8b7b05da68 Separate hide policies 2019-05-26 02:47:57 -07:00
topjohnwu
92400ebcab Process monitor minor tweaks 2019-05-26 02:35:12 -07:00
topjohnwu
23d3e56967 Add new util function 2019-05-25 21:42:51 -07:00
topjohnwu
6785dc4967 Disable verbose ptrace logging 2019-05-25 21:42:24 -07:00
topjohnwu
dad20f6a2d Update zygote namespace
Close #1492
2019-05-25 18:30:43 -07:00
topjohnwu
bb15671046 Sleep when there is nothing to wait 2019-05-25 18:17:25 -07:00
topjohnwu
21984fac8b Add API for running independent proc_monitor test 2019-05-25 16:08:53 -07:00
Viktor De Pasquale
f392afe87f Added error message in case Markdown window fails to load 2019-05-25 19:20:36 +02:00
Viktor De Pasquale
6a243ec7bc Fixed inconsistent displaying of repos and improved their sorting 2019-05-25 18:09:45 +02:00
Viktor De Pasquale
8cd3b603df Fixed cached repos not being ordered by settings 2019-05-25 18:03:32 +02:00
Viktor De Pasquale
6e1aefe6d8 Added feature that prevents repositories from being downloaded every single time that user requests to show Module/Download fragment unless requested by user 2019-05-25 16:42:34 +02:00
Viktor De Pasquale
1c90b6eca3 Fixed notification popping up every time update is scheduled 2019-05-25 16:33:55 +02:00
Viktor De Pasquale
c33cf9f878 Fixed stable channel asking for custom URL when previously selected 2019-05-25 16:15:08 +02:00
Viktor De Pasquale
27cb40eec9 Removed test options from proguard 2019-05-24 16:02:47 +02:00
Viktor De Pasquale
c06081b75d Added more proguard restrictions and rules for kotlin and moshi 2019-05-24 15:54:08 +02:00
Viktor De Pasquale
a7eec2f0a0 Fixed initial crashes occurring due to improperly obfuscated constructors and inner fields 2019-05-24 15:53:08 +02:00
Viktor De Pasquale
4fd0fe3194 Fixed repo not being correctly marked as jsonclass hence it crashed when fetching obfuscated 2019-05-24 15:51:18 +02:00
Viktor De Pasquale
cc74593ddd Removed useless constructor parameter from home vm 2019-05-24 15:50:20 +02:00
Viktor De Pasquale
fdb7c5dba1 Added Timber as marked for stripping 2019-05-24 15:49:11 +02:00
Viktor De Pasquale
77470c7cfa Updated koin 2019-05-24 12:28:57 +02:00
Viktor De Pasquale
f0a734fdab Fixed clearing cache crashing due to operations on main thread 2019-05-24 12:28:40 +02:00
topjohnwu
75405b2b25 Upgrade AS 2019-05-24 02:48:10 -07:00
osm0sis
90ed4b3c49 magiskboot: clean up remaining unneeded ELF detection bits
- default for no format match is UNSUPP_RET (unsupported) so there is no needed to explicitly detect ELF still
2019-05-24 02:46:35 -07:00
Chris Renshaw
290a17a764 magiskboot: fix bootimg hdr v2 checksum generation
- new AOSP dtb section was missing from HASH_update
2019-05-24 02:46:35 -07:00
Viktor De Pasquale
aaabd836e4 Merge remote-tracking branch 'john/master' into development 2019-05-23 20:02:29 +02:00
Viktor De Pasquale
076e5cea3b Fixed selection not persisting throughout root requests 2019-05-23 20:01:47 +02:00
Viktor De Pasquale
8515971ccf Fixed deleting "one-time" root requests whilst removing outdated 2019-05-23 19:18:16 +02:00
Viktor De Pasquale
d86fb033ea Fixed conditions being inaccurately represented 2019-05-23 19:17:41 +02:00
Viktor De Pasquale
99d7d8ddbc Fixed background being transparent for su request 2019-05-23 18:32:51 +02:00
Viktor De Pasquale
df78fd2d41 Fixed setting custom channels and switching between official ones being broken 2019-05-23 18:11:23 +02:00
Viktor De Pasquale
dabe6267b9 Fixed error that prevented flashing 2019-05-23 16:50:31 +02:00
Viktor De Pasquale
0119ebddbe Added back clearing repository cache 2019-05-23 15:28:05 +02:00
topjohnwu
3216ef9f47 Upgrade AS 2019-05-23 01:08:07 -07:00
osm0sis
b79d1bcded magiskboot: clean up remaining unneeded ELF detection bits
- default for no format match is UNSUPP_RET (unsupported) so there is no needed to explicitly detect ELF still
2019-05-21 02:49:19 -07:00
Chris Renshaw
17e234f9d5 magiskboot: fix bootimg hdr v2 checksum generation
- new AOSP dtb section was missing from HASH_update
2019-05-21 02:49:19 -07:00
Viktor De Pasquale
ea1f75f80e Merge remote-tracking branch 'john/master' into development 2019-05-20 15:10:54 +02:00
topjohnwu
8c40db5730 Don't build snet in all 2019-05-20 01:57:05 -07:00
Viktor De Pasquale
80855e89ec Merge remote-tracking branch 'john/master' into development
# Conflicts:
#	app/build.gradle
#	app/src/main/java/com/topjohnwu/magisk/model/receiver/GeneralReceiver.kt
#	app/src/main/java/com/topjohnwu/magisk/ui/hide/HideViewModel.kt
#	app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
2019-05-13 16:50:08 +02:00
Viktor De Pasquale
0850401dc4 Fixed crash where new application asks for root access 2019-05-13 15:56:27 +02:00
Viktor De Pasquale
337fda2023 Removed unnecessary classes 2019-05-13 15:41:46 +02:00
Viktor De Pasquale
64f238191e Converted constants to kotlin 2019-05-13 15:39:33 +02:00
Viktor De Pasquale
eb169cb133 Converted classmap to kotlin 2019-05-13 15:34:53 +02:00
Viktor De Pasquale
92789c3113 Added caching repositories to device 2019-05-12 20:21:55 +02:00
Viktor De Pasquale
c1c677e161 Removed old database helper 2019-05-12 19:45:07 +02:00
Viktor De Pasquale
2fe917ff82 Fixed updating values with sql 2019-05-12 19:42:05 +02:00
Viktor De Pasquale
0e6c205732 Fixed snackbar for changed su states being incorrect 2019-05-12 18:56:42 +02:00
Viktor De Pasquale
125ae0a173 Fixed conditions in sql queries 2019-05-12 18:34:28 +02:00
Viktor De Pasquale
0245e13591 Removed usage of old database object 2019-05-12 18:00:58 +02:00
Viktor De Pasquale
d546733287 Removed direct static usages of database from app 2019-05-12 17:25:26 +02:00
Viktor De Pasquale
c275326d59 Removed direct static usages of database from app 2019-05-12 16:56:56 +02:00
Viktor De Pasquale
d4561507b8 Raised deprecation level on old database 2019-05-12 14:37:24 +02:00
Viktor De Pasquale
2624706c69 Added missing repositories 2019-05-10 19:13:15 +02:00
Viktor De Pasquale
d39d885ec2 Removed repo db helper 2019-05-10 18:21:07 +02:00
Viktor De Pasquale
d83c744725 Replaced base settings fragment by its kotlin counterpart 2019-05-10 17:54:24 +02:00
Viktor De Pasquale
843995cdb9 Removed Event for good
http://bit.ly/2Ymrm61
2019-05-10 17:34:53 +02:00
Viktor De Pasquale
9491ba77e0 Removed locale manager loading languages in advance
Instead they are loaded on demand
2019-05-10 17:30:25 +02:00
Viktor De Pasquale
58a449d437 Merge branch 'remote-master' into development
# Conflicts:
#	app/src/main/java/com/topjohnwu/magisk/di/ViewModelsModule.kt
#	app/src/main/java/com/topjohnwu/magisk/ui/home/HomeViewModel.kt
#	app/src/main/java/com/topjohnwu/magisk/utils/XString.kt
2019-05-10 16:43:37 +02:00
Viktor De Pasquale
7f55e0f05b Fixed picking up wrong locale for dates 2019-05-10 16:41:31 +02:00
Viktor De Pasquale
4f9e8d2e8a Merge remote-tracking branch 'john/master' into development 2019-05-09 18:23:05 +02:00
Viktor De Pasquale
21be2f46f3 Moved fetch/toggle logic for hiding to repo
Fixed sorting
2019-05-09 18:21:38 +02:00
Viktor De Pasquale
a6e7680212 Fixed logs being bugged down by unreliable code 2019-05-09 17:38:13 +02:00
Viktor De Pasquale
e79e744e08 Fixed magisk db not returning stuff due to bad syntax 2019-05-09 17:13:02 +02:00
Viktor De Pasquale
7abdac72a4 Replaced log calls from magiskdb to repo 2019-05-09 17:01:34 +02:00
Viktor De Pasquale
90d85eaf7d Removed update repos as it can be done via repository 2019-05-09 15:56:06 +02:00
Viktor De Pasquale
e65f9740fb Updated build tools & enabled incremental kapt 2019-05-09 15:27:37 +02:00
Viktor De Pasquale
7538f89b56 Removed unnecessary calls from splash 2019-05-07 15:45:27 +02:00
Viktor De Pasquale
7c755a3991 Removed events from modules / replaced with retrofit/rx 2019-05-07 15:41:56 +02:00
Viktor De Pasquale
10e903c9fc Added direct fetch from network and fixed build issues 2019-05-06 20:12:31 +02:00
Viktor De Pasquale
b018124226 Added (ported back) features from initial design [retrofit,moshi,kotpref]
Marked most of the old classes using Networking as deprecated to clearly visualise their future removal
2019-05-06 19:03:28 +02:00
Viktor De Pasquale
5d632d0d90 Removed unnecessary overriding of observable list and replaced it copy function within observable changed callback 2019-05-05 12:46:28 +02:00
Viktor De Pasquale
4eecaea601 Fixed rewritten java code being java-styled in kotlin
Fixed accessing kotlin code illegally via companion helper
2019-05-05 12:17:32 +02:00
Viktor De Pasquale
63055818ec Fixed items in navView not being checked 2019-05-05 11:50:27 +02:00
Viktor De Pasquale
0beb08b687 Merge remote-tracking branch 'john/master' into development
# Conflicts:
#	app/src/main/java/com/topjohnwu/magisk/ui/flash/FlashViewModel.kt
2019-05-05 11:26:37 +02:00
Viktor De Pasquale
bbc9e60a12 Added scrolling to latest items while flashing
Since the adapter might be set _after_ the request, as there is no guaranteed order, it's done with waiting recursion yuck
2019-05-02 16:23:20 +02:00
Viktor De Pasquale
6c975ecc4c Fixed requesting permissions off main thread 2019-05-02 16:20:30 +02:00
Viktor De Pasquale
23e8a4ce4b Fixed shell long dumping to UI 2019-05-02 16:20:11 +02:00
Viktor De Pasquale
50134a2f9b Fixed backpress not working 2019-05-02 16:03:56 +02:00
Viktor De Pasquale
628b37c4fa Fixed icon not being tintable resulting in transparent block 2019-05-02 15:14:35 +02:00
Viktor De Pasquale
1b4ae70a43 Merge remote-tracking branch 'john/master' into development 2019-05-02 14:45:28 +02:00
Viktor De Pasquale
065051a360 Merge remote-tracking branch 'john/master' into development 2019-05-01 09:05:22 +02:00
191 changed files with 5488 additions and 4756 deletions

View File

@@ -19,16 +19,14 @@ android {
multiDexEnabled true
versionName configProps['appVersion']
versionCode configProps['appVersionCode'] as Integer
javaCompileOptions {
annotationProcessorOptions {
argument('butterknife.debuggable', 'false')
}
}
}
buildTypes {
release {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro', 'proguard-kotlin.pro'
}
}
@@ -45,6 +43,14 @@ android {
exclude '/kotlin/**'
exclude '/kotlinx/**'
}
kotlinOptions {
jvmTarget = '1.8'
}
}
androidExtensions {
experimental = true
}
dependencies {
@@ -58,26 +64,51 @@ dependencies {
implementation 'com.github.skoumalcz:teanity:0.3.3'
implementation 'com.ncapdevi:frag-nav:3.2.0'
def markwonVersion = '3.0.1'
implementation "ru.noties.markwon:core:${markwonVersion}"
implementation "ru.noties.markwon:html:${markwonVersion}"
implementation "ru.noties.markwon:image-svg:${markwonVersion}"
def vMarkwon = '3.0.1'
implementation "ru.noties.markwon:core:${vMarkwon}"
implementation "ru.noties.markwon:html:${vMarkwon}"
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
def libsuVersion = '2.5.0'
implementation "com.github.topjohnwu.libsu:core:${libsuVersion}"
implementation "com.github.topjohnwu.libsu:io:${libsuVersion}"
def vLibsu = '2.5.0'
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
def koin = "2.0.0-rc-2"
implementation "org.koin:koin-core:${koin}"
implementation "org.koin:koin-android:${koin}"
implementation "org.koin:koin-androidx-viewmodel:${koin}"
def vKoin = "2.0.1"
implementation "org.koin:koin-core:${vKoin}"
implementation "org.koin:koin-android:${vKoin}"
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
def vRetrofit = "2.6.0"
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
def vOkHttp = "3.12.3"
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
def vMoshi = "1.8.0"
implementation "com.squareup.moshi:moshi:${vMoshi}"
def vKotshi = "2.0.1"
implementation "se.ansman.kotshi:api:${vKotshi}"
kapt "se.ansman.kotshi:compiler:${vKotshi}"
modules {
module('androidx.room:room-runtime') {
replacedBy('com.github.topjohnwu:room-runtime')
}
}
def vRoom = "2.1.0"
implementation "com.github.topjohnwu:room-runtime:${vRoom}"
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.browser:browser:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha06'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'com.google.android.material:material:1.1.0-alpha06'
implementation 'androidx.work:work-runtime:2.0.1'
implementation 'androidx.transition:transition:1.2.0-alpha01'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha07'
}

20
app/proguard-kotlin.pro Normal file
View File

@@ -0,0 +1,20 @@
## So every class is case insensitive to avoid some bizare problems
-dontusemixedcaseclassnames
## If reflection issues come up uncomment this, that should temporarily fix it
#-keep class kotlin.** { *; }
#-keep class kotlin.Metadata { *; }
#-keepclassmembers class kotlin.Metadata {
# public <methods>;
#}
## Never warn about Kotlin, it should work as-is
-dontwarn kotlin.**
## Removes runtime null checks - doesn't really matter if it crashes on kotlin or java NPE
-assumenosideeffects class kotlin.jvm.internal.Intrinsics {
static void checkParameterIsNotNull(java.lang.Object, java.lang.String);
}
## Useless option for dex
-dontpreverify

View File

@@ -35,6 +35,7 @@
-keepclassmembers class com.topjohnwu.signing.BootSigner { *; }
# Strip logging
-assumenosideeffects class timber.log.Timber.Tree { *; }
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
public *** debug(...);
}

View File

@@ -4,22 +4,21 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatDelegate
import androidx.multidex.MultiDex
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import androidx.room.Room
import androidx.work.impl.WorkDatabase
import androidx.work.impl.WorkDatabase_Impl
import com.topjohnwu.magisk.di.koinModules
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.inject
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
import timber.log.Timber
@@ -29,13 +28,6 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
lateinit var protectedContext: Context
@Deprecated("Use dependency injection")
val prefs: SharedPreferences by inject()
@Deprecated("Use dependency injection")
val DB: MagiskDB by inject()
@Deprecated("Use dependency injection")
val repoDB: RepoDatabaseHelper by inject()
@Volatile
private var foreground: Activity? = null
@@ -118,6 +110,12 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
Shell.Config.addInitializers(RootUtils::class.java)
Shell.Config.setTimeout(2)
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
Room.setFactory {
when (it) {
WorkDatabase::class.java -> WorkDatabase_Impl()
else -> null
}
}
}
@Deprecated("")

View File

@@ -1,31 +0,0 @@
package com.topjohnwu.magisk;
import com.topjohnwu.magisk.model.download.DownloadModuleService;
import com.topjohnwu.magisk.model.receiver.GeneralReceiver;
import com.topjohnwu.magisk.model.update.UpdateCheckService;
import com.topjohnwu.magisk.ui.MainActivity;
import com.topjohnwu.magisk.ui.SplashActivity;
import com.topjohnwu.magisk.ui.flash.FlashActivity;
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity;
import java.util.HashMap;
import java.util.Map;
public class ClassMap {
private static Map<Class, Class> classMap = new HashMap<>();
static {
classMap.put(App.class, a.e.class);
classMap.put(MainActivity.class, a.b.class);
classMap.put(SplashActivity.class, a.c.class);
classMap.put(FlashActivity.class, a.f.class);
classMap.put(UpdateCheckService.class, a.g.class);
classMap.put(GeneralReceiver.class, a.h.class);
classMap.put(DownloadModuleService.class, a.j.class);
classMap.put(SuRequestActivity.class, a.m.class);
}
public static <T> Class<T> get(Class c) {
return classMap.get(c);
}
}

View File

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

View File

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

View File

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

View File

@@ -1,102 +0,0 @@
package com.topjohnwu.magisk;
import android.os.Environment;
import android.os.Process;
import java.io.File;
public class Const {
public static final String DEBUG_TAG = "MagiskManager";
// APK content
public static final String ANDROID_MANIFEST = "AndroidManifest.xml";
public static final String SU_KEYSTORE_KEY = "su_key";
// Paths
public static final String MAGISK_PATH = "/sbin/.magisk/img";
public static final File EXTERNAL_PATH;
public static File MAGISK_DISABLE_FILE;
static {
MAGISK_DISABLE_FILE = new File("xxx");
EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
EXTERNAL_PATH.mkdirs();
}
public static final String TMP_FOLDER_PATH = "/dev/tmp";
public static final String MAGISK_LOG = "/cache/magisk.log";
public static final String MANAGER_CONFIGS = ".tmp.magisk.config";
// Versions
public static final int UPDATE_SERVICE_VER = 1;
public static final int SNET_EXT_VER = 12;
public static final int USER_ID = Process.myUid() / 100000;
// Generic
public static final String MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log";
public static final class MAGISK_VER {
public static final int MIN_SUPPORT = 18000;
}
public static class ID {
public static final int FETCH_ZIP = 2;
public static final int SELECT_BOOT = 3;
// notifications
public static final int MAGISK_UPDATE_NOTIFICATION_ID = 4;
public static final int APK_UPDATE_NOTIFICATION_ID = 5;
public static final int DTBO_NOTIFICATION_ID = 7;
public static final int HIDE_MANAGER_NOTIFICATION_ID = 8;
public static final String UPDATE_NOTIFICATION_CHANNEL = "update";
public static final String PROGRESS_NOTIFICATION_CHANNEL = "progress";
public static final String CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update";
}
public static class Url {
private static String getRaw(String where, String name) {
return String.format("https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s", where, name);
}
public static final String STABLE_URL = getRaw("master", "stable.json");
public static final String BETA_URL = getRaw("master", "beta.json");
public static final String CANARY_URL = getRaw("master", "canary_builds/release.json");
public static final String CANARY_DEBUG_URL = getRaw("master", "canary_builds/canary.json");
public static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d";
public static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
public static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
public static final String MODULE_INSTALLER = "https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh";
public static final String PAYPAL_URL = "https://www.paypal.me/topjohnwu";
public static final String PATREON_URL = "https://www.patreon.com/topjohnwu";
public static final String TWITTER_URL = "https://twitter.com/topjohnwu";
public static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
public static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk";
public static final String SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk");
public static final String BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl");
}
public static class Key {
// others
public static final String LINK_KEY = "Link";
public static final String IF_NONE_MATCH = "If-None-Match";
// intents
public static final String OPEN_SECTION = "section";
public static final String INTENT_SET_NAME = "filename";
public static final String INTENT_SET_LINK = "link";
public static final String FLASH_ACTION = "action";
public static final String BROADCAST_MANAGER_UPDATE = "manager_update";
public static final String BROADCAST_REBOOT = "reboot";
}
public static class Value {
public static final String FLASH_ZIP = "flash";
public static final String PATCH_FILE = "patch";
public static final String FLASH_MAGISK = "magisk";
public static final String FLASH_INACTIVE_SLOT = "slot";
public static final String UNINSTALL = "uninstall";
}
}

View File

@@ -0,0 +1,98 @@
package com.topjohnwu.magisk
import android.os.Environment
import android.os.Process
import java.io.File
object Const {
const val DEBUG_TAG = "MagiskManager"
// Paths
const val MAGISK_PATH = "/sbin/.magisk/img"
@JvmField
val EXTERNAL_PATH = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)!!
@JvmField
var MAGISK_DISABLE_FILE = File("xxx")
const val TMP_FOLDER_PATH = "/dev/tmp"
const val MAGISK_LOG = "/cache/magisk.log"
// Versions
const val SNET_EXT_VER = 12
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
// Misc
const val ANDROID_MANIFEST = "AndroidManifest.xml"
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
const val MANAGER_CONFIGS = ".tmp.magisk.config"
@JvmField
val USER_ID = Process.myUid() / 100000
init {
EXTERNAL_PATH.mkdirs()
}
object MagiskVersion {
const val MIN_SUPPORT = 18000
}
object ID {
const val FETCH_ZIP = 2
const val SELECT_BOOT = 3
// notifications
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
const val APK_UPDATE_NOTIFICATION_ID = 5
const val DTBO_NOTIFICATION_ID = 7
const val HIDE_MANAGER_NOTIFICATION_ID = 8
const val UPDATE_NOTIFICATION_CHANNEL = "update"
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
}
object Url {
@Deprecated("This shouldn't be used. There's literally no need for it")
const val REPO_URL =
"https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d"
const val FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s"
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
const val MODULE_INSTALLER =
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
const val TWITTER_URL = "https://twitter.com/topjohnwu"
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
@JvmField
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
private fun getRaw(where: String, name: String) =
"${GITHUB_RAW_API_URL}topjohnwu/magisk_files/$where/$name"
}
object Key {
// others
const val LINK_KEY = "Link"
const val IF_NONE_MATCH = "If-None-Match"
// intents
const val OPEN_SECTION = "section"
const val INTENT_SET_NAME = "filename"
const val INTENT_SET_LINK = "link"
const val FLASH_ACTION = "action"
const val BROADCAST_MANAGER_UPDATE = "manager_update"
const val BROADCAST_REBOOT = "reboot"
}
object Value {
const val FLASH_ZIP = "flash"
const val PATCH_FILE = "patch"
const val FLASH_MAGISK = "magisk"
const val FLASH_INACTIVE_SLOT = "slot"
const val UNINSTALL = "uninstall"
}
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.model.entity.UpdateInfo;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.ShellUtils;
public final class Info {
public static int magiskVersionCode = -1;
@NonNull
public static String magiskVersionString = "";
public static UpdateInfo remote = new UpdateInfo();
public static boolean keepVerity = false;
public static boolean keepEnc = false;
public static boolean recovery = false;
public static void loadMagiskInfo() {
try {
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":")[0];
magiskVersionCode = Integer.parseInt(ShellUtils.fastCmd("magisk -V"));
Config.setMagiskHide(Shell.su("magiskhide --status").exec().isSuccess());
} catch (NumberFormatException ignored) {
}
}
}

View File

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

View File

@@ -1,190 +0,0 @@
package com.topjohnwu.magisk.data.database;
import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.text.TextUtils;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.model.entity.Policy;
import com.topjohnwu.magisk.model.entity.SuLogEntry;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.superuser.Shell;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
public class MagiskDB {
private static final String POLICY_TABLE = "policies";
private static final String LOG_TABLE = "logs";
private static final String SETTINGS_TABLE = "settings";
private static final String STRINGS_TABLE = "strings";
private PackageManager pm;
public MagiskDB(Context context) {
pm = context.getPackageManager();
}
public void deletePolicy(Policy policy) {
deletePolicy(policy.uid);
}
private List<String> rawSQL(String fmt, Object... args) {
return Shell.su("magisk --sqlite '" + Utils.fmt(fmt, args) + "'").exec().getOut();
}
private List<ContentValues> SQL(String fmt, Object... args) {
List<ContentValues> list = new ArrayList<>();
for (String raw : rawSQL(fmt, args)) {
ContentValues values = new ContentValues();
String[] cols = raw.split("\\|");
for (String col : cols) {
String[] pair = col.split("=", 2);
if (pair.length != 2)
continue;
values.put(pair[0], pair[1]);
}
list.add(values);
}
return list;
}
private String toSQL(ContentValues values) {
StringBuilder keys = new StringBuilder(), vals = new StringBuilder();
keys.append('(');
vals.append("VALUES(");
boolean first = true;
for (Map.Entry<String, Object> entry : values.valueSet()) {
if (!first) {
keys.append(',');
vals.append(',');
} else {
first = false;
}
keys.append(entry.getKey());
vals.append('"');
vals.append(entry.getValue());
vals.append('"');
}
keys.append(')');
vals.append(')');
keys.append(vals);
return keys.toString();
}
public void clearOutdated() {
rawSQL(
"DELETE FROM %s WHERE until > 0 AND until < %d;" +
"DELETE FROM %s WHERE time < %d",
POLICY_TABLE, System.currentTimeMillis() / 1000,
LOG_TABLE, System.currentTimeMillis() - Config.suLogTimeout * 86400000
);
}
public void deletePolicy(String pkg) {
rawSQL("DELETE FROM %s WHERE package_name=\"%s\"", POLICY_TABLE, pkg);
}
public void deletePolicy(int uid) {
rawSQL("DELETE FROM %s WHERE uid=%d", POLICY_TABLE, uid);
}
public Policy getPolicy(int uid) {
List<ContentValues> res =
SQL("SELECT * FROM %s WHERE uid=%d", POLICY_TABLE, uid);
if (!res.isEmpty()) {
try {
return new Policy(res.get(0), pm);
} catch (PackageManager.NameNotFoundException e) {
deletePolicy(uid);
}
}
return null;
}
public void updatePolicy(Policy policy) {
rawSQL("REPLACE INTO %s %s", POLICY_TABLE, toSQL(policy.getContentValues()));
}
public List<Policy> getPolicyList() {
List<Policy> list = new ArrayList<>();
for (ContentValues values : SQL("SELECT * FROM %s WHERE uid/100000=%d", POLICY_TABLE, Const.USER_ID)) {
try {
list.add(new Policy(values, pm));
} catch (PackageManager.NameNotFoundException e) {
deletePolicy(values.getAsInteger("uid"));
}
}
Collections.sort(list);
return list;
}
public List<List<SuLogEntry>> getLogs() {
List<List<SuLogEntry>> ret = new ArrayList<>();
List<SuLogEntry> list = null;
String dateString = null, newString;
for (ContentValues values : SQL("SELECT * FROM %s ORDER BY time DESC", LOG_TABLE)) {
Date date = new Date(values.getAsLong("time"));
newString = DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
if (!TextUtils.equals(dateString, newString)) {
dateString = newString;
list = new ArrayList<>();
ret.add(list);
}
list.add(new SuLogEntry(values));
}
return ret;
}
public void addLog(SuLogEntry log) {
rawSQL("INSERT INTO %s %s", LOG_TABLE, toSQL(log.getContentValues()));
}
public void clearLogs() {
rawSQL("DELETE FROM %s", LOG_TABLE);
}
public void rmSettings(String key) {
rawSQL("DELETE FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
}
public void setSettings(String key, int value) {
ContentValues data = new ContentValues();
data.put("key", key);
data.put("value", value);
rawSQL("REPLACE INTO %s %s", SETTINGS_TABLE, toSQL(data));
}
public int getSettings(String key, int defaultValue) {
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", SETTINGS_TABLE, key);
if (res.isEmpty())
return defaultValue;
return res.get(0).getAsInteger("value");
}
public void setStrings(String key, String value) {
if (value == null) {
rawSQL("DELETE FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
return;
}
ContentValues data = new ContentValues();
data.put("key", key);
data.put("value", value);
rawSQL("REPLACE INTO %s %s", STRINGS_TABLE, toSQL(data));
}
public String getStrings(String key, String defaultValue) {
List<ContentValues> res = SQL("SELECT value FROM %s WHERE key=\"%s\"", STRINGS_TABLE, key);
if (res.isEmpty())
return defaultValue;
return res.get(0).getAsString("value");
}
}

View File

@@ -0,0 +1,81 @@
package com.topjohnwu.magisk.data.database
import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.base.*
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.toMap
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.utils.now
import timber.log.Timber
import java.util.concurrent.TimeUnit
class PolicyDao(
private val context: Context
) : BaseDao() {
override val table: String = DatabaseDefinition.Table.POLICY
fun deleteOutdated(
nowSeconds: Long = TimeUnit.MILLISECONDS.toSeconds(now)
) = query<Delete> {
condition {
greaterThan("until", "0")
and {
lessThan("until", nowSeconds.toString())
}
or {
lessThan("until", "0")
}
}
}.ignoreElement()
fun delete(packageName: String) = query<Delete> {
condition {
equals("package_name", packageName)
}
}.ignoreElement()
fun delete(uid: Int) = query<Delete> {
condition {
equals("uid", uid)
}
}.ignoreElement()
fun fetch(uid: Int) = query<Select> {
condition {
equals("uid", uid)
}
}.map { it.first().toPolicySafe() }
fun update(policy: MagiskPolicy) = query<Replace> {
values(policy.toMap())
}.ignoreElement()
fun fetchAll() = query<Select> {
condition {
equals("uid/100000", Const.USER_ID)
}
}.map { it.mapNotNull { it.toPolicySafe() } }
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
val taskResult = runCatching { toPolicy(context.packageManager) }
val result = taskResult.getOrNull()
val exception = taskResult.exceptionOrNull()
Timber.e(exception)
when (exception) {
is PackageManager.NameNotFoundException -> {
val uid = getOrElse("uid") { null } ?: return null
delete(uid).subscribe()
}
}
return result
}
}

View File

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

View File

@@ -0,0 +1,109 @@
package com.topjohnwu.magisk.data.database
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import androidx.core.content.edit
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.entity.Repo
import java.util.*
@Deprecated("")
class RepoDatabaseHelper
constructor(context: Context) : SQLiteOpenHelper(context, "repo.db", null, DATABASE_VER) {
private val mDb: SQLiteDatabase = writableDatabase
val rawCursor: Cursor
@Deprecated("")
get() = mDb.query(TABLE_NAME, null, null, null, null, null, null)
val repoCursor: Cursor
@Deprecated("")
get() {
var orderBy: String? = null
when (Config.repoOrder) {
Config.Value.ORDER_NAME -> orderBy = "name COLLATE NOCASE"
Config.Value.ORDER_DATE -> orderBy = "last_update DESC"
}
return mDb.query(TABLE_NAME, null, null, null, null, null, orderBy)
}
val repoIDSet: Set<String>
@Deprecated("")
get() {
val set = HashSet<String>(300)
mDb.query(TABLE_NAME, null, null, null, null, null, null).use { c ->
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex("id")))
}
}
return set
}
override fun onCreate(db: SQLiteDatabase) {
onUpgrade(db, 0, DATABASE_VER)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion != newVersion) {
// Nuke old DB and create new table
db.execSQL("DROP TABLE IF EXISTS $TABLE_NAME")
db.execSQL(
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
"author TEXT, description TEXT, last_update INT, PRIMARY KEY(id))")
Config.prefs.edit {
remove(Config.Key.ETAG_KEY)
}
}
}
override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
onUpgrade(db, 0, DATABASE_VER)
}
@Deprecated("")
fun clearRepo() {
mDb.delete(TABLE_NAME, null, null)
}
@Deprecated("")
fun removeRepo(id: String) {
mDb.delete(TABLE_NAME, "id=?", arrayOf(id))
}
@Deprecated("")
fun removeRepo(repo: Repo) {
removeRepo(repo.id)
}
@Deprecated("")
fun removeRepo(list: Iterable<String>) {
list.forEach {
mDb.delete(TABLE_NAME, "id=?", arrayOf(it))
}
}
@Deprecated("")
fun addRepo(repo: Repo) {
mDb.replace(TABLE_NAME, null, repo.contentValues)
}
@Deprecated("")
fun getRepo(id: String): Repo? {
mDb.query(TABLE_NAME, null, "id=?", arrayOf(id), null, null, null).use { c ->
if (c.moveToNext()) {
return Repo(c)
}
}
return null
}
companion object {
private val DATABASE_VER = 5
private val TABLE_NAME = "repos"
}
}

View File

@@ -0,0 +1,22 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
class SettingsDao : BaseDao() {
override val table = DatabaseDefinition.Table.SETTINGS
fun delete(key: String) = query<Delete> {
condition { equals("key", key) }
}.ignoreElement()
fun put(key: String, value: Int) = query<Replace> {
values("key" to key, "value" to value)
}.ignoreElement()
fun fetch(key: String, default: Int = -1) = query<Select> {
fields("value")
condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull()?.toIntOrNull() ?: default }
}

View File

@@ -0,0 +1,22 @@
package com.topjohnwu.magisk.data.database
import com.topjohnwu.magisk.data.database.base.*
class StringDao : BaseDao() {
override val table = DatabaseDefinition.Table.STRINGS
fun delete(key: String) = query<Delete> {
condition { equals("key", key) }
}.ignoreElement()
fun put(key: String, value: String) = query<Replace> {
values("key" to key, "value" to value)
}.ignoreElement()
fun fetch(key: String, default: String = "") = query<Select> {
fields("value")
condition { equals("key", key) }
}.map { it.firstOrNull()?.values?.firstOrNull() ?: default }
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,155 @@
package com.topjohnwu.magisk.data.database.base
import androidx.annotation.StringDef
import com.topjohnwu.magisk.data.database.base.Order.Companion.ASC
import com.topjohnwu.magisk.data.database.base.Order.Companion.DESC
interface MagiskQueryBuilder {
val requestType: String
var table: String
companion object {
inline operator fun <reified Builder : MagiskQueryBuilder> invoke(builder: Builder.() -> Unit): MagiskQuery =
Builder::class.java.newInstance()
.apply(builder)
.toString()
.let {
MagiskQuery(it)
}
}
}
class Delete : MagiskQueryBuilder {
override val requestType: String = "DELETE FROM"
override var table = ""
private var condition = ""
fun condition(builder: Condition.() -> Unit) {
condition = Condition().apply(builder).toString()
}
override fun toString(): String {
return listOf(requestType, table, condition).joinToString(" ")
}
}
class Select : MagiskQueryBuilder {
override val requestType: String get() = "SELECT $fields FROM"
override lateinit var table: String
private var fields = "*"
private var condition = ""
private var orderField = ""
fun fields(vararg newFields: String) {
if (newFields.isEmpty()) {
fields = "*"
return
}
fields = newFields.joinToString(", ")
}
fun condition(builder: Condition.() -> Unit) {
condition = Condition().apply(builder).toString()
}
fun orderBy(field: String, @OrderStrict order: String) {
orderField = "ORDER BY $field $order"
}
override fun toString(): String {
return listOf(requestType, table, condition, orderField).joinToString(" ")
}
}
class Replace : Insert() {
override val requestType: String = "REPLACE INTO"
}
open class Insert : MagiskQueryBuilder {
override val requestType: String = "INSERT INTO"
override lateinit var table: String
private val keys get() = _values.keys.joinToString(",")
private val values get() = _values.values.joinToString(",") {
when (it) {
is Boolean -> if (it) "1" else "0"
is Number -> it.toString()
else -> "\"$it\""
}
}
private var _values: Map<String, Any> = mapOf()
fun values(vararg pairs: Pair<String, Any>) {
_values = pairs.toMap()
}
fun values(values: Map<String, Any>) {
_values = values
}
override fun toString(): String {
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
}
}
class Condition {
private val conditionWord = "WHERE %s"
private var condition: String = ""
fun equals(field: String, value: Any) {
condition = when (value) {
is String -> "$field=\"$value\""
else -> "$field=$value"
}
}
fun greaterThan(field: String, value: String) {
condition = "$field > $value"
}
fun lessThan(field: String, value: String) {
condition = "$field < $value"
}
fun greaterOrEqualTo(field: String, value: String) {
condition = "$field >= $value"
}
fun lessOrEqualTo(field: String, value: String) {
condition = "$field <= $value"
}
fun and(builder: Condition.() -> Unit) {
condition = "($condition AND ${Condition().apply(builder).condition})"
}
fun or(builder: Condition.() -> Unit) {
condition = "($condition OR ${Condition().apply(builder).condition})"
}
override fun toString(): String {
return conditionWord.format(condition)
}
}
class Order {
@set:OrderStrict
var order = DESC
var field = ""
companion object {
const val ASC = "ASC"
const val DESC = "DESC"
}
}
@StringDef(ASC, DESC)
@Retention(AnnotationRetention.SOURCE)
annotation class OrderStrict

View File

@@ -0,0 +1,62 @@
package com.topjohnwu.magisk.data.network
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.model.entity.UpdateInfo
import io.reactivex.Single
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Streaming
import retrofit2.http.Url
interface GithubRawApiServices {
//region topjohnwu/magisk_files
@GET("$MAGISK_FILES/master/stable.json")
fun fetchStableUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/beta.json")
fun fetchBetaUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/release.json")
fun fetchCanaryUpdate(): Single<UpdateInfo>
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
fun fetchCanaryDebugUpdate(): Single<UpdateInfo>
@GET
fun fetchCustomUpdate(@Url url: String): Single<UpdateInfo>
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
@Streaming
fun fetchSafetynet(@Path(REVISION) revision: String = Const.SNET_REVISION): Single<ResponseBody>
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
@Streaming
fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): Single<ResponseBody>
//endregion
/**
* This method shall be used exclusively for fetching files from urls from previous requests.
* Him, who uses it in a wrong way, shall die in an eternal flame.
* */
@GET
@Streaming
fun fetchFile(@Url url: String): Single<ResponseBody>
companion object {
private const val REVISION = "revision"
private const val MODULE = "module"
private const val FILE = "file"
private const val MAGISK_FILES = "topjohnwu/magisk_files"
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
}
}

View File

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

View File

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

View File

@@ -0,0 +1,40 @@
package com.topjohnwu.magisk.data.repository
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.LogDao
import com.topjohnwu.magisk.data.database.base.suRaw
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.superuser.Shell
import java.util.concurrent.TimeUnit
class LogRepository(
private val logDao: LogDao
) {
fun fetchLogs() = logDao.fetchAll()
.map { it.sortByDescending { it.date.time }; it }
.map { it.wrap() }
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
.filter { it.isNotEmpty() }
fun clearLogs() = logDao.deleteAll()
fun clearOutdated() = logDao.deleteOutdated()
fun clearMagiskLogs() = Shell.su("echo -n > " + Const.MAGISK_LOG)
.toSingle()
.map { it.exec() }
fun put(log: MagiskLog) = logDao.put(log)
private fun List<MagiskLog>.wrap(): List<WrappedMagiskLog> {
val day = TimeUnit.DAYS.toMillis(1)
return groupBy { it.date.time / day }
.map { WrappedMagiskLog(it.key * day, it.value) }
}
}

View File

@@ -0,0 +1,104 @@
package com.topjohnwu.magisk.data.repository
import android.content.Context
import android.content.pm.PackageManager
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.network.GithubRawApiServices
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.writeToFile
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
class MagiskRepository(
private val context: Context,
private val apiRaw: GithubRawApiServices,
private val packageManager: PackageManager
) {
fun fetchMagisk() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.magisk.link) }
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
fun fetchManager() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.app.link) }
.map { it.writeToFile(context, FILE_MAGISK_APK) }
fun fetchUninstaller() = fetchUpdate()
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
fun fetchSafetynet() = apiRaw.fetchSafetynet()
fun fetchBootctl() = apiRaw
.fetchBootctl()
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
fun fetchUpdate() = when (Config.updateChannel) {
Config.Value.DEFAULT_CHANNEL, Config.Value.STABLE_CHANNEL -> apiRaw.fetchStableUpdate()
Config.Value.BETA_CHANNEL -> apiRaw.fetchBetaUpdate()
Config.Value.CANARY_CHANNEL -> apiRaw.fetchCanaryUpdate()
Config.Value.CANARY_DEBUG_CHANNEL -> apiRaw.fetchCanaryDebugUpdate()
Config.Value.CUSTOM_CHANNEL -> apiRaw.fetchCustomUpdate(Config.customChannelUrl)
else -> throw IllegalArgumentException()
}.flatMap {
// If remote version is lower than current installed, try switching to beta
if (it.magisk.versionCode < Info.magiskVersionCode
&& Config.updateChannel == Config.Value.DEFAULT_CHANNEL) {
Config.updateChannel = Config.Value.BETA_CHANNEL
apiRaw.fetchBetaUpdate()
} else {
Single.just(it)
}
}.map { Info.remote = it; it }
fun fetchApps() =
Single.fromCallable { packageManager.getInstalledApplications(0) }
.flattenAsFlowable { it }
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map {
val label = Utils.getAppLabel(it, packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}
.filter { it.processes.isNotEmpty() }
.toList()
fun fetchHideTargets() = Shell.su("magiskhide --ls").toSingle()
.map { it.exec().out }
.flattenAsFlowable { it }
.map { HideTarget(it) }
.toList()
fun toggleHide(isEnabled: Boolean, packageName: String, process: String) =
"magiskhide --%s %s %s".format(isEnabled.state, packageName, process).su().ignoreElement()
private val Boolean.state get() = if (this) "add" else "rm"
companion object {
const val FILE_MAGISK_ZIP = "magisk.zip"
const val FILE_MAGISK_APK = "magisk.apk"
const val FILE_UNINSTALLER_ZIP = "uninstaller.zip"
const val FILE_SAFETY_NET_APK = "safetynet.apk"
const val FILE_BOOTCTL_SH = "bootctl"
private val blacklist = listOf(
let { val app: App by inject(); app }.packageName,
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
)
}
}

View File

@@ -12,8 +12,7 @@ val applicationModule = module {
factory { get<Context>().resources }
factory { get<Context>() as App }
factory { get<Context>().packageManager }
single(SUTimeout) {
get<App>().protectedContext.getSharedPreferences("su_timeout", 0)
}
single { PreferenceManager.getDefaultSharedPreferences(get<App>().protectedContext) }
}
factory(Protected) { get<App>().protectedContext }
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
}

View File

@@ -1,12 +1,15 @@
package com.topjohnwu.magisk.di
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.data.database.*
import com.topjohnwu.magisk.tasks.UpdateRepos
import org.koin.dsl.module
val databaseModule = module {
single { MagiskDB(get<App>().protectedContext) }
single { LogDao() }
single { PolicyDao(get()) }
single { SettingsDao() }
single { StringDao() }
single { RepoDatabaseHelper(get()) }
single { UpdateRepos(get()) }
}

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ val viewModelModules = module {
viewModel { HomeViewModel(get()) }
viewModel { SuperuserViewModel(get(), get(), get(), get()) }
viewModel { HideViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get()) }
viewModel { ModuleViewModel(get(), get(), get()) }
viewModel { LogViewModel(get(), get()) }
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }

View File

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

View File

@@ -6,6 +6,8 @@ import android.content.Intent;
import android.net.Uri;
import android.os.IBinder;
import androidx.annotation.Nullable;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Const;
@@ -29,8 +31,6 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import androidx.annotation.Nullable;
public class DownloadModuleService extends Service {
private List<ProgressNotification> notifications;

View File

@@ -5,10 +5,10 @@ import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.List;
import androidx.annotation.NonNull;
import java.util.List;
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
private String mId, mName, mVersion, mAuthor, mDescription;
@@ -36,9 +36,18 @@ public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
mVersionCode = p.readInt();
}
protected BaseModule(MagiskModule m) {
mId = m.getId();
mName = m.getName();
mVersion = m.getVersion();
mAuthor = m.getAuthor();
mDescription = m.getDescription();
mVersionCode = Integer.parseInt(m.getVersionCode());
}
@Override
public int compareTo(@NonNull BaseModule module) {
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
}
@Override
@@ -71,7 +80,9 @@ public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
return values;
}
protected void parseProps(List<String> props) { parseProps(props.toArray(new String[0])); }
protected void parseProps(List<String> props) {
parseProps(props.toArray(new String[0]));
}
protected void parseProps(String[] props) throws NumberFormatException {
for (String line : props) {

View File

@@ -0,0 +1,57 @@
package com.topjohnwu.magisk.model.entity
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
import com.topjohnwu.magisk.utils.timeFormatTime
import com.topjohnwu.magisk.utils.toTime
import java.util.*
data class MagiskLog(
val fromUid: Int,
val toUid: Int,
val fromPid: Int,
val packageName: String,
val appName: String,
val command: String,
val action: Boolean,
val date: Date
) {
val timeString = date.time.toTime(timeFormatTime)
}
data class WrappedMagiskLog(
val time: Long,
val items: List<MagiskLog>
)
fun Map<String, String>.toLog(): MagiskLog {
return MagiskLog(
fromUid = get("from_uid")?.toIntOrNull() ?: -1,
toUid = get("to_uid")?.toIntOrNull() ?: -1,
fromPid = get("from_pid")?.toIntOrNull() ?: -1,
packageName = get("package_name").orEmpty(),
appName = get("app_name").orEmpty(),
command = get("command").orEmpty(),
action = get("action")?.toIntOrNull() != 0,
date = get("time")?.toLongOrNull()?.toDate() ?: Date()
)
}
fun Long.toDate() = Date(this)
fun MagiskLog.toMap() = mapOf(
"from_uid" to fromUid,
"to_uid" to toUid,
"from_pid" to fromPid,
"package_name" to packageName,
"app_name" to appName,
"command" to command,
"action" to action,
"time" to date.time
)
fun MagiskPolicy.toLog(
toUid: Int,
fromPid: Int,
command: String,
date: Date
) = MagiskLog(uid, toUid, fromPid, packageName, appName, command, policy == ALLOW, date)

View File

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

View File

@@ -0,0 +1,68 @@
package com.topjohnwu.magisk.model.entity
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
data class MagiskPolicy(
val uid: Int,
val packageName: String,
val appName: String,
val policy: Int = INTERACTIVE,
val until: Long = -1L,
val logging: Boolean = true,
val notification: Boolean = true,
val applicationInfo: ApplicationInfo
) {
companion object {
const val INTERACTIVE = 0
const val DENY = 1
const val ALLOW = 2
}
}
fun MagiskPolicy.toMap() = mapOf(
"uid" to uid,
"package_name" to packageName,
"policy" to policy,
"until" to until,
"logging" to logging,
"notification" to notification
)
@Throws(PackageManager.NameNotFoundException::class)
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
val uid = get("uid")?.toIntOrNull() ?: -1
val packageName = get("package_name").orEmpty()
val info = pm.getApplicationInfo(packageName, 0)
if (info.uid != uid)
throw PackageManager.NameNotFoundException()
return MagiskPolicy(
uid = uid,
packageName = packageName,
policy = get("policy")?.toIntOrNull() ?: INTERACTIVE,
until = get("until")?.toLongOrNull() ?: -1L,
logging = get("logging")?.toIntOrNull() != 0,
notification = get("notification")?.toIntOrNull() != 0,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
)
}
@Throws(PackageManager.NameNotFoundException::class)
fun Int.toPolicy(pm: PackageManager): MagiskPolicy {
val pkg = pm.getPackagesForUid(this)?.firstOrNull()
?: throw PackageManager.NameNotFoundException()
val info = pm.getApplicationInfo(pkg, 0)
return MagiskPolicy(
uid = this,
packageName = pkg,
applicationInfo = info,
appName = info.loadLabel(pm).toString()
)
}

View File

@@ -6,16 +6,33 @@ import android.os.Parcelable;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
public class Module extends BaseModule {
public class OldModule extends BaseModule {
private SuFile mRemoveFile, mDisableFile, mUpdateFile;
private boolean mEnable, mRemove, mUpdated;
public static final Parcelable.Creator<OldModule> CREATOR = new Creator<OldModule>() {
/* It won't be used at any place */
@Override
public OldModule createFromParcel(Parcel source) {
return null;
}
public Module(String path) {
@Override
public OldModule[] newArray(int size) {
return null;
}
};
private final SuFile mRemoveFile;
private final SuFile mDisableFile;
private final SuFile mUpdateFile;
private final boolean mUpdated;
private boolean mEnable;
private boolean mRemove;
public OldModule(String path) {
try {
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
} catch (NumberFormatException ignored) {}
} catch (NumberFormatException ignored) {
}
mRemoveFile = new SuFile(path, "remove");
mDisableFile = new SuFile(path, "disable");
@@ -35,19 +52,6 @@ public class Module extends BaseModule {
mUpdated = mUpdateFile.exists();
}
public static final Parcelable.Creator<Module> CREATOR = new Creator<Module>() {
/* It won't be used at any place */
@Override
public Module createFromParcel(Parcel source) {
return null;
}
@Override
public Module[] newArray(int size) {
return null;
}
};
public void createDisableFile() {
mEnable = !mDisableFile.createNewFile();
}

View File

@@ -4,10 +4,10 @@ import android.content.ContentValues;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import com.topjohnwu.magisk.utils.Utils;
import androidx.annotation.NonNull;
import com.topjohnwu.magisk.utils.Utils;
public class Policy implements Comparable<Policy>{
public static final int INTERACTIVE = 0;
@@ -27,7 +27,7 @@ public class Policy implements Comparable<Policy>{
this.uid = uid;
packageName = pkgs[0];
info = pm.getApplicationInfo(packageName, 0);
appName = Utils.getAppLabel(info, pm);
appName = Utils.INSTANCE.getAppLabel(info, pm);
}
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {

View File

@@ -29,6 +29,11 @@ public class Repo extends BaseModule {
mLastUpdate = new Date(p.readLong());
}
public Repo(Repository repo) {
super(repo);
mLastUpdate = new Date(repo.getLastUpdate());
}
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
@Override
@@ -49,7 +54,7 @@ public class Repo extends BaseModule {
}
public void update() throws IllegalRepoException {
String props[] = Utils.dlString(getPropUrl()).split("\\n");
String[] props = Utils.INSTANCE.dlString(getPropUrl()).split("\\n");
try {
parseProps(props);
} catch (NumberFormatException e) {
@@ -98,7 +103,7 @@ public class Repo extends BaseModule {
}
public String getDownloadFilename() {
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
return Utils.INSTANCE.getLegalFilename(getName() + "-" + getVersion() + ".zip");
}
public class IllegalRepoException extends Exception {

View File

@@ -15,11 +15,11 @@ public class SuLogEntry {
public boolean action;
public Date date;
public SuLogEntry(Policy policy) {
fromUid = policy.uid;
packageName = policy.packageName;
appName = policy.appName;
action = policy.policy == Policy.ALLOW;
public SuLogEntry(MagiskPolicy policy) {
fromUid = policy.getUid();
packageName = policy.getPackageName();
appName = policy.getAppName();
action = policy.getPolicy() == Policy.ALLOW;
}
public SuLogEntry(ContentValues values) {
@@ -47,10 +47,10 @@ public class SuLogEntry {
}
public String getDateString() {
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale).format(date);
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.getLocale()).format(date);
}
public String getTimeString() {
return new SimpleDateFormat("h:mm a", LocaleManager.locale).format(date);
return new SimpleDateFormat("h:mm a", LocaleManager.getLocale()).format(date);
}
}

View File

@@ -0,0 +1,33 @@
package com.topjohnwu.magisk.model.entity
import com.squareup.moshi.Json
import se.ansman.kotshi.JsonSerializable
@JsonSerializable
data class UpdateInfo(
val app: ManagerJson = ManagerJson(),
val uninstaller: UninstallerJson = UninstallerJson(),
val magisk: MagiskJson = MagiskJson()
)
@JsonSerializable
data class UninstallerJson(
val link: String = ""
)
@JsonSerializable
data class MagiskJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = "",
@Json(name = "md5") val hash: String = ""
)
@JsonSerializable
data class ManagerJson(
val version: String = "",
val versionCode: Int = -1,
val link: String = "",
val note: String = ""
)

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,13 @@
package com.topjohnwu.magisk.model.entity.recycler
import androidx.databinding.ObservableList
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.SuLogEntry
import com.topjohnwu.magisk.model.entity.MagiskLog
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
import com.topjohnwu.magisk.utils.timeFormatMedium
import com.topjohnwu.magisk.utils.toTime
import com.topjohnwu.magisk.utils.toggle
class LogRvItem : ComparableRvItem<LogRvItem>() {
@@ -25,12 +27,12 @@ class LogRvItem : ComparableRvItem<LogRvItem>() {
}
class LogItemRvItem(
val items: ObservableList<ComparableRvItem<*>>
item: WrappedMagiskLog
) : ComparableRvItem<LogItemRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log
val date = items.filterIsInstance<LogItemEntryRvItem>().firstOrNull()
?.item?.dateString.orEmpty()
val date = item.time.toTime(timeFormatMedium)
val items: List<ComparableRvItem<*>> = item.items.map { LogItemEntryRvItem(it) }
val isExpanded = KObservableField(false)
fun toggle() = isExpanded.toggle()
@@ -41,7 +43,7 @@ class LogItemRvItem(
override fun itemSameAs(other: LogItemRvItem): Boolean = date == other.date
}
class LogItemEntryRvItem(val item: SuLogEntry) : ComparableRvItem<LogItemEntryRvItem>() {
class LogItemEntryRvItem(val item: MagiskLog) : ComparableRvItem<LogItemEntryRvItem>() {
override val layoutRes: Int = R.layout.item_superuser_log_entry
val isExpanded = KObservableField(false)

View File

@@ -6,12 +6,13 @@ import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.Module
import com.topjohnwu.magisk.model.entity.OldModule
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.Repository
import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.utils.toggle
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
override val layoutRes: Int = R.layout.item_module
@@ -49,18 +50,22 @@ class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
override fun contentSameAs(other: ModuleRvItem): Boolean = item.version == other.item.version
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
&& item.name == other.item.name
override fun itemSameAs(other: ModuleRvItem): Boolean = item.name == other.item.name
override fun itemSameAs(other: ModuleRvItem): Boolean = item.id == other.item.id
}
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
constructor(repo: Repository) : this(Repo(repo))
override val layoutRes: Int = R.layout.item_repo
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version
&& item.lastUpdate == other.item.lastUpdate
&& item.versionCode == other.item.versionCode
&& item.description == other.item.description
&& item.detailUrl == other.item.detailUrl
override fun itemSameAs(other: RepoRvItem): Boolean = item.detailUrl == other.item.detailUrl
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
}

View File

@@ -6,13 +6,14 @@ import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.toggle
class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
override val layoutRes: Int = R.layout.item_policy
@@ -25,6 +26,13 @@ class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<Poli
private val rxBus: RxBus by inject()
private val currentStateItem
get() = item.copy(
policy = if (isEnabled.value) Policy.ALLOW else Policy.DENY,
notification = shouldNotify.value,
logging = shouldLog.value
)
init {
isEnabled.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
@@ -32,13 +40,11 @@ class PolicyRvItem(val item: Policy, val icon: Drawable) : ComparableRvItem<Poli
}
shouldNotify.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
item.notification = it
rxBus.post(PolicyUpdateEvent.Notification(this@PolicyRvItem))
rxBus.post(PolicyUpdateEvent.Notification(currentStateItem))
}
shouldLog.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
item.logging = it
rxBus.post(PolicyUpdateEvent.Log(this@PolicyRvItem))
rxBus.post(PolicyUpdateEvent.Log(currentStateItem))
}
}

View File

@@ -1,6 +1,7 @@
package com.topjohnwu.magisk.model.events
import com.skoumal.teanity.rxbus.RxBus
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
@@ -8,9 +9,9 @@ import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
class HideProcessEvent(val item: HideProcessRvItem) : RxBus.Event
class PolicyEnableEvent(val item: PolicyRvItem, val enable: Boolean) : RxBus.Event
sealed class PolicyUpdateEvent(val item: PolicyRvItem) : RxBus.Event {
class Notification(item: PolicyRvItem) : PolicyUpdateEvent(item)
class Log(item: PolicyRvItem) : PolicyUpdateEvent(item)
sealed class PolicyUpdateEvent(val item: MagiskPolicy) : RxBus.Event {
class Notification(item: MagiskPolicy) : PolicyUpdateEvent(item)
class Log(item: MagiskPolicy) : PolicyUpdateEvent(item)
}
class ModuleUpdatedEvent(val item: ModuleRvItem) : RxBus.Event

View File

@@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import androidx.core.os.postDelayed
import com.topjohnwu.magisk.tasks.FlashZip
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.internal.UiThreadHandler
@@ -33,7 +32,7 @@ sealed class Flashing(
override fun onResult(success: Boolean) {
if (success) {
Utils.loadModules()
//Utils.loadModules()
}
super.onResult(success)
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class BooleanProperty(
private val name: String,
private val default: Boolean,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Boolean> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Boolean {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Boolean
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class FloatProperty(
private val name: String,
private val default: Float,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Float> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Float {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Float
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class IntProperty(
private val name: String,
private val default: Int,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Int> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Int {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Int
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LongProperty(
private val name: String,
private val default: Long,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Long> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Long {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Long
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -0,0 +1,69 @@
package com.topjohnwu.magisk.model.preference
import android.content.Context
import android.content.SharedPreferences
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
interface PreferenceModel {
val context: Context
val fileName: String
get() = "${context.packageName}_preferences"
val commitPrefs: Boolean
get() = false
val prefs: SharedPreferences
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
fun preferenceStrInt(
name: String,
default: Int,
writeDefault: Boolean = false,
commit: Boolean = commitPrefs
) = object: ReadWriteProperty<PreferenceModel, Int> {
val base = StringProperty(name, default.toString(), commit)
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
base.getValue(thisRef, property).toInt()
override fun setValue(thisRef: PreferenceModel, property: KProperty<*>, value: Int) =
base.setValue(thisRef, property, value.toString())
}
fun preference(
name: String,
default: Boolean,
commit: Boolean = commitPrefs
) = BooleanProperty(name, default, commit)
fun preference(
name: String,
default: Float,
commit: Boolean = commitPrefs
) = FloatProperty(name, default, commit)
fun preference(
name: String,
default: Int,
commit: Boolean = commitPrefs
) = IntProperty(name, default, commit)
fun preference(
name: String,
default: Long,
commit: Boolean = commitPrefs
) = LongProperty(name, default, commit)
fun preference(
name: String,
default: String,
commit: Boolean = commitPrefs
) = StringProperty(name, default, commit)
fun preference(
name: String,
default: Set<String>,
commit: Boolean = commitPrefs
) = StringSetProperty(name, default, commit)
}

View File

@@ -0,0 +1,21 @@
package com.topjohnwu.magisk.model.preference
import android.content.SharedPreferences
abstract class Property {
fun SharedPreferences.Editor.put(name: String, value: Boolean) = putBoolean(name, value)
fun SharedPreferences.Editor.put(name: String, value: Float) = putFloat(name, value)
fun SharedPreferences.Editor.put(name: String, value: Int) = putInt(name, value)
fun SharedPreferences.Editor.put(name: String, value: Long) = putLong(name, value)
fun SharedPreferences.Editor.put(name: String, value: String) = putString(name, value)
fun SharedPreferences.Editor.put(name: String, value: Set<String>) = putStringSet(name, value)
fun SharedPreferences.get(name: String, value: Boolean) = getBoolean(name, value)
fun SharedPreferences.get(name: String, value: Float) = getFloat(name, value)
fun SharedPreferences.get(name: String, value: Int) = getInt(name, value)
fun SharedPreferences.get(name: String, value: Long) = getLong(name, value)
fun SharedPreferences.get(name: String, value: String) = getString(name, value) ?: value
fun SharedPreferences.get(name: String, value: Set<String>) = getStringSet(name, value) ?: value
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class StringProperty(
private val name: String,
private val default: String,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, String> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): String {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: String
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.preference
import androidx.core.content.edit
import com.topjohnwu.magisk.utils.trimEmptyToNull
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class StringSetProperty(
private val name: String,
private val default: Set<String>,
private val commit: Boolean
) : Property(), ReadWriteProperty<PreferenceModel, Set<String>> {
override operator fun getValue(
thisRef: PreferenceModel,
property: KProperty<*>
): Set<String> {
val prefName = name.trimEmptyToNull() ?: property.name
return thisRef.prefs.get(prefName, default)
}
override operator fun setValue(
thisRef: PreferenceModel,
property: KProperty<*>,
value: Set<String>
) {
val prefName = name.trimEmptyToNull() ?: property.name
thisRef.prefs.edit(commit) { put(prefName, value) }
}
}

View File

@@ -6,18 +6,22 @@ import android.content.Intent
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.database.base.su
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
import com.topjohnwu.magisk.utils.DownloadApp
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.SuLogger
import com.topjohnwu.magisk.utils.get
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.utils.reboot
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.superuser.Shell
open class GeneralReceiver : BroadcastReceiver() {
private val appRepo: AppRepository by inject()
companion object {
const val REQUEST = "request"
const val LOG = "log"
@@ -32,7 +36,6 @@ open class GeneralReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent == null)
return
val mDB: MagiskDB = get()
var action: String? = intent.action ?: return
when (action) {
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
@@ -47,7 +50,7 @@ open class GeneralReceiver : BroadcastReceiver() {
}
when (action) {
REQUEST -> {
val i = Intent(context, ClassMap.get<Any>(SuRequestActivity::class.java))
val i = Intent(context, ClassMap[SuRequestActivity::class.java])
.setAction(action)
.putExtra("socket", intent.getStringExtra("socket"))
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@@ -61,20 +64,20 @@ open class GeneralReceiver : BroadcastReceiver() {
}
Intent.ACTION_PACKAGE_REPLACED ->
// This will only work pre-O
if (Config.get<Boolean>(Config.Key.SU_REAUTH)!!) {
mDB.deletePolicy(getPkg(intent))
}
if (Config.suReAuth)
appRepo.delete(getPkg(intent)).blockingGet()
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
val pkg = getPkg(intent)
mDB.deletePolicy(pkg)
Shell.su("magiskhide --rm $pkg").submit()
appRepo.delete(pkg).blockingGet()
"magiskhide --rm $pkg".su().blockingGet()
}
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
Const.Key.BROADCAST_MANAGER_UPDATE -> {
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK)
Info.remote = Info.remote.copy(app = Info.remote.app.copy(
link = intent.getStringExtra(Const.Key.INTENT_SET_LINK) ?: ""))
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME))
}
Const.Key.BROADCAST_REBOOT -> RootUtils.reboot()
Const.Key.BROADCAST_REBOOT -> reboot()
}
}
}

View File

@@ -1,33 +0,0 @@
package com.topjohnwu.magisk.model.update;
import androidx.annotation.NonNull;
import androidx.work.ListenableWorker;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.model.worker.DelegateWorker;
import com.topjohnwu.magisk.tasks.CheckUpdates;
import com.topjohnwu.magisk.view.Notifications;
import com.topjohnwu.superuser.Shell;
public class UpdateCheckService extends DelegateWorker {
@NonNull
@Override
public ListenableWorker.Result doWork() {
if (App.foreground() == null) {
Shell.getShell();
CheckUpdates.check(this::onCheckDone);
}
return ListenableWorker.Result.success();
}
private void onCheckDone() {
if (BuildConfig.VERSION_CODE < Config.remoteManagerVersionCode) {
Notifications.managerUpdate();
} else if (Config.magiskVersionCode < Config.remoteMagiskVersionCode) {
Notifications.magiskUpdate();
}
}
}

View File

@@ -0,0 +1,30 @@
package com.topjohnwu.magisk.model.update
import androidx.work.ListenableWorker
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.model.worker.DelegateWorker
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.superuser.Shell
class UpdateCheckService : DelegateWorker() {
private val magiskRepo: MagiskRepository by inject()
override fun doWork(): ListenableWorker.Result {
// Make sure shell initializer was ran
Shell.getShell()
return runCatching {
magiskRepo.fetchUpdate().blockingGet()
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
Notifications.managerUpdate()
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
Notifications.magiskUpdate()
ListenableWorker.Result.success()
}.getOrElse {
ListenableWorker.Result.failure()
}
}
}

View File

@@ -4,12 +4,6 @@ import android.content.Context;
import android.net.Network;
import android.net.Uri;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -17,6 +11,12 @@ import androidx.annotation.RequiresApi;
import androidx.work.Data;
import androidx.work.ListenableWorker;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;
import java.util.Set;
import java.util.UUID;
public abstract class DelegateWorker {
private ListenableWorker worker;

View File

@@ -0,0 +1,75 @@
package com.topjohnwu.magisk.model.zip
import com.topjohnwu.magisk.utils.forEach
import com.topjohnwu.magisk.utils.withStreams
import com.topjohnwu.superuser.io.SuFile
import java.io.File
import java.util.zip.ZipInputStream
class Zip private constructor(private val values: Builder) {
companion object {
operator fun invoke(builder: Builder.() -> Unit): Zip {
return Zip(Builder().apply(builder))
}
}
class Builder {
lateinit var zip: File
lateinit var destination: File
var excludeDirs = true
}
data class Path(val path: String, val pullFromDir: Boolean = true)
fun unzip(vararg paths: Pair<String, Boolean>) =
unzip(*paths.map { Path(it.first, it.second) }.toTypedArray())
@Suppress("RedundantLambdaArrow")
fun unzip(vararg paths: Path) {
ensureRequiredParams()
values.zip.zipStream().use {
it.forEach { e ->
val currentPath = paths.firstOrNull { e.name.startsWith(it.path) }
val isDirectory = values.excludeDirs && e.isDirectory
if (currentPath == null || isDirectory) {
// Ignore directories, only create files
return@forEach
}
val name = if (currentPath.pullFromDir) {
e.name.substring(e.name.lastIndexOf('/') + 1)
} else {
e.name
}
val out = File(values.destination, name)
.ensureExists()
.outputStream()
//.suOutputStream()
withStreams(it, out) { reader, writer ->
reader.copyTo(writer)
}
}
}
}
private fun ensureRequiredParams() {
if (!values.zip.exists()) {
throw RuntimeException("Zip file does not exist")
}
}
private fun File.ensureExists() =
if ((!parentFile.exists() && !parentFile.mkdirs()) || parentFile is SuFile) {
SuFile(parentFile, name).apply { parentFile.mkdirs() }
} else {
this
}
private fun File.zipStream() = ZipInputStream(inputStream())
}

View File

@@ -1,132 +0,0 @@
package com.topjohnwu.magisk.tasks;
import android.os.SystemClock;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.net.Networking;
import com.topjohnwu.net.Request;
import com.topjohnwu.net.ResponseListener;
import com.topjohnwu.superuser.ShellUtils;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import org.json.JSONException;
import org.json.JSONObject;
public class CheckUpdates {
private static Request getRequest() {
String url;
switch ((int) Config.get(Config.Key.UPDATE_CHANNEL)) {
case Config.Value.BETA_CHANNEL:
url = Const.Url.BETA_URL;
break;
case Config.Value.CUSTOM_CHANNEL:
url = Config.get(Config.Key.CUSTOM_CHANNEL);
break;
case Config.Value.CANARY_CHANNEL:
url = Const.Url.CANARY_URL;
break;
case Config.Value.CANARY_DEBUG_CHANNEL:
url = Const.Url.CANARY_DEBUG_URL;
break;
default:
url = Const.Url.STABLE_URL;
break;
}
return Networking.get(url);
}
public static void check() {
check(null);
}
public static void check(Runnable cb) {
Request request = getRequest();
UpdateListener listener = new UpdateListener(cb);
if (ShellUtils.onMainThread()) {
request.getAsJSONObject(listener);
} else {
JSONObject json = request.execForJSONObject().getResult();
if (json != null)
listener.onResponse(json);
}
}
private static class UpdateListener implements ResponseListener<JSONObject> {
private Runnable cb;
private long start;
UpdateListener(Runnable callback) {
cb = callback;
start = SystemClock.uptimeMillis();
}
private int getInt(JSONObject json, String name, int defValue) {
if (json == null)
return defValue;
try {
return json.getInt(name);
} catch (JSONException e) {
return defValue;
}
}
private String getString(JSONObject json, String name, String defValue) {
if (json == null)
return defValue;
try {
return json.getString(name);
} catch (JSONException e) {
return defValue;
}
}
private JSONObject getJson(JSONObject json, String name) {
try {
return json.getJSONObject(name);
} catch (JSONException e) {
return null;
}
}
@Override
public void onResponse(JSONObject json) {
JSONObject magisk = getJson(json, "magisk");
Config.remoteMagiskVersionCode = getInt(magisk, "versionCode", -1);
if ((int) Config.get(Config.Key.UPDATE_CHANNEL) == Config.Value.DEFAULT_CHANNEL) {
if (Config.magiskVersionCode > Config.remoteMagiskVersionCode) {
// If we are newer than current stable channel, switch to beta
Config.set(Config.Key.UPDATE_CHANNEL, Config.Value.BETA_CHANNEL);
check(cb);
return;
} else {
Config.set(Config.Key.UPDATE_CHANNEL, Config.Value.STABLE_CHANNEL);
}
}
Config.remoteMagiskVersionString = getString(magisk, "version", null);
Config.magiskLink = getString(magisk, "link", null);
Config.magiskNoteLink = getString(magisk, "note", null);
Config.magiskMD5 = getString(magisk, "md5", null);
JSONObject manager = getJson(json, "app");
Config.remoteManagerVersionString = getString(manager, "version", null);
Config.remoteManagerVersionCode = getInt(manager, "versionCode", -1);
Config.managerLink = getString(manager, "link", null);
Config.managerNoteLink = getString(manager, "note", null);
JSONObject uninstaller = getJson(json, "uninstaller");
Config.uninstallerLink = getString(uninstaller, "link", null);
UiThreadHandler.handler.postAtTime(() -> Event.trigger(Event.UPDATE_CHECK_DONE),
start + 1000 /* Add artificial delay to let UI behave correctly */);
if (cb != null)
cb.run();
}
}
}

View File

@@ -8,8 +8,8 @@ import androidx.annotation.MainThread;
import androidx.annotation.WorkerThread;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.Info;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.DownloadProgressListener;
import com.topjohnwu.net.Networking;
@@ -123,9 +123,9 @@ public abstract class MagiskInstaller {
File zip = new File(App.self.getCacheDir(), "magisk.zip");
if (!ShellUtils.checkSum("MD5", zip, Config.magiskMD5)) {
if (!ShellUtils.checkSum("MD5", zip, Info.remote.getMagisk().getHash())) {
console.add("- Downloading zip");
Networking.get(Config.magiskLink)
Networking.get(Info.remote.getMagisk().getLink())
.setDownloadProgressListener(new ProgressLog())
.execForFile(zip);
} else {
@@ -282,10 +282,10 @@ public abstract class MagiskInstaller {
return false;
}
if (!Shell.sh(Utils.fmt(
if (!Shell.sh(Utils.INSTANCE.fmt(
"KEEPFORCEENCRYPT=%b KEEPVERITY=%b RECOVERYMODE=%b " +
"sh update-binary sh boot_patch.sh %s",
Config.keepEnc, Config.keepVerity, Config.recovery, srcBoot))
Info.keepEnc, Info.keepVerity, Info.recovery, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
@@ -311,10 +311,10 @@ public abstract class MagiskInstaller {
}
protected boolean flashBoot() {
if (!Shell.su(Utils.fmt("direct_install %s %s", installDir, srcBoot))
if (!Shell.su(Utils.INSTANCE.fmt("direct_install %s %s", installDir, srcBoot))
.to(console, logs).exec().isSuccess())
return false;
if (!Config.keepVerity)
if (!Info.keepVerity)
Shell.su("patch_dtbo_image").to(console, logs).exec();
return true;
}

View File

@@ -6,8 +6,8 @@ import android.util.Pair;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper;
import com.topjohnwu.magisk.model.entity.Repo;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.Logger;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.net.Networking;
@@ -18,7 +18,6 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.net.HttpURLConnection;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collections;
@@ -31,16 +30,19 @@ import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
public class UpdateRepos {
private static final DateFormat DATE_FORMAT;
import androidx.annotation.NonNull;
import io.reactivex.Single;
private final App app = App.self;
@Deprecated
public class UpdateRepos {
@NonNull
private final RepoDatabaseHelper repoDB;
private Set<String> cached;
private Queue<Pair<String, Date>> moduleQueue;
static {
DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
public UpdateRepos(@NonNull RepoDatabaseHelper repoDatabase) {
repoDB = repoDatabase;
}
private void runTasks(Runnable task) {
@@ -54,20 +56,32 @@ public class UpdateRepos {
f.get();
} catch (InterruptedException e) {
continue;
} catch (ExecutionException ignored) {}
} catch (ExecutionException ignored) {
}
break;
}
}
}
/**
* Static instance of (Simple)DateFormat is not threadsafe so in order to make it safe it needs
* to be created beforehand on the same thread where it'll be used.
* See https://stackoverflow.com/a/18383395
*/
private static SimpleDateFormat getDateFormat() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format;
}
/* We sort repos by last push, which means that we only need to check whether the
* first page is updated to determine whether the online repo database is changed
*/
private boolean parsePage(int page) {
Request req = Networking.get(Utils.fmt(Const.Url.REPO_URL, page + 1));
Request req = Networking.get(Utils.INSTANCE.fmt(Const.Url.REPO_URL, page + 1));
if (page == 0) {
String etag = Config.get(Config.Key.ETAG_KEY);
if (etag != null)
String etag = Config.getEtagKey();
if (!etag.isEmpty())
req.addHeaders(Const.Key.IF_NONE_MATCH, etag);
}
Request.Result<JSONArray> res = req.execForJSONArray();
@@ -84,10 +98,12 @@ public class UpdateRepos {
return true;
try {
SimpleDateFormat dateFormat = getDateFormat();
for (int i = 0; i < res.getResult().length(); i++) {
JSONObject rawRepo = res.getResult().getJSONObject(i);
String id = rawRepo.getString("name");
Date date = DATE_FORMAT.parse(rawRepo.getString("pushed_at"));
Date date = dateFormat.parse(rawRepo.getString("pushed_at"));
moduleQueue.offer(new Pair<>(id, date));
}
} catch (JSONException | ParseException e) {
@@ -100,7 +116,7 @@ public class UpdateRepos {
String etag = res.getConnection().getHeaderField(Config.Key.ETAG_KEY);
if (etag != null) {
etag = etag.substring(etag.indexOf('\"'), etag.lastIndexOf('\"') + 1);
Config.set(Config.Key.ETAG_KEY, etag);
Config.setEtagKey(etag);
}
}
@@ -116,17 +132,17 @@ public class UpdateRepos {
Pair<String, Date> pair = moduleQueue.poll();
if (pair == null)
return;
Repo repo = app.getRepoDB().getRepo(pair.first);
Repo repo = repoDB.getRepo(pair.first);
try {
if (repo == null)
repo = new Repo(pair.first);
else
cached.remove(pair.first);
repo.update(pair.second);
app.getRepoDB().addRepo(repo);
repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
app.getRepoDB().removeRepo(pair.first);
repoDB.removeRepo(pair.first);
}
}
});
@@ -134,7 +150,7 @@ public class UpdateRepos {
}
private void fullReload() {
Cursor c = app.getRepoDB().getRawCursor();
Cursor c = repoDB.getRawCursor();
runTasks(() -> {
while (true) {
Repo repo;
@@ -145,32 +161,31 @@ public class UpdateRepos {
}
try {
repo.update();
app.getRepoDB().addRepo(repo);
repoDB.addRepo(repo);
} catch (Repo.IllegalRepoException e) {
Logger.debug(e.getMessage());
app.getRepoDB().removeRepo(repo);
repoDB.removeRepo(repo);
}
}
});
}
public void exec(boolean force) {
Event.reset(Event.REPO_LOAD_DONE);
App.THREAD_POOL.execute(() -> {
cached = Collections.synchronizedSet(app.getRepoDB().getRepoIDSet());
public Single<Boolean> exec(boolean force) {
return Single.fromCallable(() -> {
cached = Collections.synchronizedSet(repoDB.getRepoIDSet());
moduleQueue = new ConcurrentLinkedQueue<>();
if (loadPages()) {
// The leftover cached means they are removed from online repo
app.getRepoDB().removeRepo(cached);
repoDB.removeRepo(cached);
} else if (force) {
fullReload();
}
Event.trigger(Event.REPO_LOAD_DONE);
return force; // not important
});
}
public void exec() {
exec(false);
public Single<Boolean> exec() {
return exec(false);
}
}

View File

@@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment
import com.topjohnwu.magisk.ClassMap
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const.Key.OPEN_SECTION
import com.topjohnwu.magisk.Info
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.databinding.ActivityMainBinding
import com.topjohnwu.magisk.model.navigation.Navigation
@@ -48,7 +49,7 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
override fun onCreate(savedInstanceState: Bundle?) {
if (!SplashActivity.DONE) {
startActivity(Intent(this, ClassMap.get<Any>(SplashActivity::class.java)))
startActivity(Intent(this, ClassMap[SplashActivity::class.java]))
finish()
}
@@ -105,11 +106,11 @@ open class MainActivity : MagiskActivity<MainViewModel, ActivityMainBinding>() {
private fun checkHideSection() {
val menu = binding.navView.menu
menu.findItem(R.id.magiskHideFragment).isVisible =
Shell.rootAccess() && Config.get<Any>(Config.Key.MAGISKHIDE) as Boolean
Shell.rootAccess() && Config.magiskHide
menu.findItem(R.id.modulesFragment).isVisible =
Shell.rootAccess() && Config.magiskVersionCode >= 0
Shell.rootAccess() && Info.magiskVersionCode >= 0
menu.findItem(R.id.reposFragment).isVisible =
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Config.magiskVersionCode >= 0)
(Networking.checkNetworkStatus(this) && Shell.rootAccess() && Info.magiskVersionCode >= 0)
menu.findItem(R.id.logFragment).isVisible =
Shell.rootAccess()
menu.findItem(R.id.superuserFragment).isVisible =

View File

@@ -6,14 +6,12 @@ import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.tasks.CheckUpdates
import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.data.database.SettingsDao
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.view.Notifications
import com.topjohnwu.magisk.view.Shortcuts
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.get
open class SplashActivity : AppCompatActivity() {
@@ -21,7 +19,7 @@ open class SplashActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
Shell.getShell {
if (Config.magiskVersionCode > 0 && Config.magiskVersionCode < Const.MAGISK_VER.MIN_SUPPORT) {
if (Info.magiskVersionCode > 0 && Info.magiskVersionCode < Const.MagiskVersion.MIN_SUPPORT) {
AlertDialog.Builder(this)
.setTitle(R.string.unsupport_magisk_title)
.setMessage(R.string.unsupport_magisk_message)
@@ -35,9 +33,9 @@ open class SplashActivity : AppCompatActivity() {
}
private fun initAndStart() {
val pkg = Config.get<String>(Config.Key.SU_MANAGER)
if (pkg != null && packageName == BuildConfig.APPLICATION_ID) {
Config.remove(Config.Key.SU_MANAGER)
val pkg = Config.suManager
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
get<SettingsDao>().delete(Config.Key.SU_MANAGER)
Shell.su("pm uninstall $pkg").submit()
}
if (TextUtils.equals(pkg, packageName)) {
@@ -48,9 +46,6 @@ open class SplashActivity : AppCompatActivity() {
}
}
// Dynamic detect all locales
LocaleManager.loadAvailableLocales(R.string.app_changelog)
// Set default configs
Config.initialize()
@@ -59,21 +54,11 @@ open class SplashActivity : AppCompatActivity() {
// Schedule periodic update checks
Utils.scheduleUpdateCheck()
CheckUpdates.check()
// Setup shortcuts
Shortcuts.setup(this)
// Magisk working as expected
if (Shell.rootAccess() && Config.magiskVersionCode > 0) {
// Load modules
Utils.loadModules(false)
// Load repos
if (Networking.checkNetworkStatus(this))
UpdateRepos().exec()
}
val intent = Intent(this, ClassMap.get<Any>(MainActivity::class.java))
val intent = Intent(this, ClassMap[MainActivity::class.java])
intent.putExtra(Const.Key.OPEN_SECTION, getIntent().getStringExtra(Const.Key.OPEN_SECTION))
DONE = true
startActivity(intent)

View File

@@ -1,7 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
interface ActivityResultListener {
fun onActivityResult(resultCode: Int, data: Intent?)
}

View File

@@ -1,81 +0,0 @@
package com.topjohnwu.magisk.ui.base;
import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.utils.Event;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroupAdapter;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView;
public abstract class BasePreferenceFragment extends PreferenceFragmentCompat
implements SharedPreferences.OnSharedPreferenceChangeListener, Event.AutoListener {
public App app = App.self;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = super.onCreateView(inflater, container, savedInstanceState);
app.getPrefs().registerOnSharedPreferenceChangeListener(this);
Event.register(this);
return v;
}
@Override
public void onDestroyView() {
app.getPrefs().unregisterOnSharedPreferenceChangeListener(this);
Event.unregister(this);
super.onDestroyView();
}
@Override
public int[] getListeningEvents() {
return new int[0];
}
@Override
protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
return new PreferenceGroupAdapter(preferenceScreen) {
@SuppressLint("RestrictedApi")
@Override
public void onBindViewHolder(PreferenceViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
Preference preference = getItem(position);
if (preference instanceof PreferenceCategory)
setZeroPaddingToLayoutChildren(holder.itemView);
else {
View iconFrame = holder.itemView.findViewById(R.id.icon_frame);
if (iconFrame != null) {
iconFrame.setVisibility(preference.getIcon() == null ? View.GONE : View.VISIBLE);
}
}
}
};
}
private void setZeroPaddingToLayoutChildren(View view) {
if (!(view instanceof ViewGroup))
return;
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
setZeroPaddingToLayoutChildren(viewGroup.getChildAt(i));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1)
viewGroup.setPaddingRelative(0, viewGroup.getPaddingTop(), viewGroup.getPaddingEnd(), viewGroup.getPaddingBottom());
else
viewGroup.setPadding(0, viewGroup.getPaddingTop(), viewGroup.getPaddingRight(), viewGroup.getPaddingBottom());
}
}
}

View File

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

View File

@@ -1,11 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.content.Intent
interface IBaseLeanback {
fun runWithExternalRW(callback: Runnable)
fun runWithPermissions(vararg permissions: String, callback: Runnable)
fun startActivityForResult(intent: Intent, requestCode: Int, listener: ActivityResultListener)
}

View File

@@ -1,10 +1,13 @@
package com.topjohnwu.magisk.ui.base
import android.Manifest
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatDelegate
import androidx.collection.SparseArrayCompat
import androidx.core.net.toUri
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
@@ -16,6 +19,7 @@ import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.ncapdevi.fragnav.FragNavController
import com.ncapdevi.fragnav.FragNavTransactionOptions
import com.skoumal.teanity.view.TeanityActivity
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.model.events.BackPressEvent
@@ -27,16 +31,20 @@ import com.topjohnwu.magisk.model.navigation.Navigator
import com.topjohnwu.magisk.model.permissions.PermissionRequestBuilder
import com.topjohnwu.magisk.utils.LocaleManager
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.set
import timber.log.Timber
import kotlin.reflect.KClass
typealias RequestCallback = MagiskActivity<*, *>.(Int, Intent?) -> Unit
abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
MagiskLeanbackActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
Navigator, FragNavController.TransactionListener {
TeanityActivity<ViewModel, Binding>(), FragNavController.RootFragmentListener,
Navigator, FragNavController.TransactionListener {
override val numberOfRootFragments: Int get() = baseFragments.size
override val baseFragments: List<KClass<out Fragment>> = listOf()
private val resultCallbacks = SparseArrayCompat<RequestCallback>()
protected open val defaultPosition: Int = 0
@@ -50,14 +58,12 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
get() = navigationController?.let { it.currentStackIndex != defaultPosition } ?: false
init {
val isDarkTheme = Config.get<Boolean>(Config.Key.DARK_THEME)
val theme = if (isDarkTheme) {
val theme = if (Config.darkTheme) {
AppCompatDelegate.MODE_NIGHT_YES
} else {
AppCompatDelegate.MODE_NIGHT_NO
}
AppCompatDelegate.setDefaultNightMode(theme)
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
}
override fun applyOverrideConfiguration(config: Configuration?) {
@@ -66,6 +72,10 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
super.applyOverrideConfiguration(config)
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(LocaleManager.getLocaleContext(base, LocaleManager.locale))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
navigationController?.apply {
@@ -185,19 +195,23 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) =
if (report?.areAllPermissionsGranted() == true) {
override fun onPermissionsChecked(report: MultiplePermissionsReport) {
if (report.areAllPermissionsGranted()) {
request.onSuccess()
} else {
request.onFailure()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) = request.onShowRationale(permissions.orEmpty().map { it.name })
})
.check()
permissions: MutableList<PermissionRequest>,
token: PermissionToken
) = token.continuePermissionRequest()
}).check()
}
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
withPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
}
private fun FragNavTransactionOptions.Builder.customAnimations(options: MagiskAnimBuilder) =
@@ -207,4 +221,21 @@ abstract class MagiskActivity<ViewModel : MagiskViewModel, Binding : ViewDataBin
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultCallbacks[requestCode]?.apply {
resultCallbacks.remove(requestCode)
invoke(this@MagiskActivity, resultCode, data)
}
}
fun startActivityForResult(
intent: Intent,
requestCode: Int,
listener: RequestCallback
) {
resultCallbacks[requestCode] = listener
startActivityForResult(intent, requestCode)
}
}

View File

@@ -1,64 +0,0 @@
package com.topjohnwu.magisk.ui.base
import android.Manifest
import android.content.Intent
import androidx.collection.SparseArrayCompat
import androidx.databinding.ViewDataBinding
import com.karumi.dexter.Dexter
import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import com.skoumal.teanity.view.TeanityActivity
import com.topjohnwu.magisk.Const
abstract class MagiskLeanbackActivity<ViewModel : MagiskViewModel, Binding : ViewDataBinding> :
TeanityActivity<ViewModel, Binding>(), IBaseLeanback {
private val resultListeners = SparseArrayCompat<ActivityResultListener>()
@Deprecated("Permissions will be checked in a different streamlined way")
fun runWithExternalRW(callback: () -> Unit) = runWithExternalRW(Runnable { callback() })
@Deprecated("Permissions will be checked in a different streamlined way")
override fun runWithExternalRW(callback: Runnable) {
runWithPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE, callback = callback)
}
@Deprecated("Permissions will be checked in a different streamlined way")
override fun runWithPermissions(vararg permissions: String, callback: Runnable) {
Dexter.withActivity(this)
.withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport?) {
if (report?.areAllPermissionsGranted() == true) {
Const.EXTERNAL_PATH.mkdirs()
callback.run()
}
}
override fun onPermissionRationaleShouldBeShown(
permissions: MutableList<PermissionRequest>?,
token: PermissionToken?
) = Unit
})
.check()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
resultListeners.get(requestCode)?.apply {
resultListeners.remove(requestCode)
onActivityResult(resultCode, data)
}
}
override fun startActivityForResult(
intent: Intent,
requestCode: Int,
listener: ActivityResultListener
) {
resultListeners.put(requestCode, listener)
startActivityForResult(intent, requestCode)
}
}

View File

@@ -6,16 +6,11 @@ import com.skoumal.teanity.viewmodel.LoadingViewModel
import com.topjohnwu.magisk.model.events.BackPressEvent
import com.topjohnwu.magisk.model.events.PermissionEvent
import com.topjohnwu.magisk.model.events.ViewActionEvent
import com.topjohnwu.magisk.utils.Event
import io.reactivex.Observable
import io.reactivex.subjects.PublishSubject
import timber.log.Timber
abstract class MagiskViewModel : LoadingViewModel(), Event.AutoListener {
override fun onEvent(event: Int) = Timber.i("Event of $event was not handled")
override fun getListeningEvents(): IntArray = intArrayOf()
abstract class MagiskViewModel : LoadingViewModel() {
fun withView(action: Activity.() -> Unit) {
ViewActionEvent(action).publish()

View File

@@ -103,7 +103,7 @@ class FlashViewModel(
.subscribeK { SnackbarEvent(it).publish() }
.add()
fun restartPressed() = RootUtils.reboot()
fun restartPressed() = reboot()
fun backPressed() = back()

View File

@@ -1,32 +1,26 @@
package com.topjohnwu.magisk.ui.hide
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.rxbus.RxBus
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.model.entity.HideAppInfo
import com.topjohnwu.magisk.model.entity.HideTarget
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.model.entity.recycler.HideProcessRvItem
import com.topjohnwu.magisk.model.entity.recycler.HideRvItem
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
import com.topjohnwu.magisk.model.events.HideProcessEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.update
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import me.tatarka.bindingcollectionadapter2.OnItemBind
import timber.log.Timber
class HideViewModel(
private val packageManager: PackageManager,
private val magiskRepo: MagiskRepository,
rxBus: RxBus
) : MagiskViewModel() {
@@ -55,26 +49,19 @@ class HideViewModel(
// fetching this for every item is nonsensical, so we add .cache() so the response is all
// the same for every single mapped item, it only actually executes the whole thing the
// first time around.
val hideTargets = Shell.su("magiskhide --ls").toSingle()
.map { it.exec().out }
.flattenAsFlowable { it }
.map { HideTarget(it) }
.toList()
.cache()
val hideTargets = magiskRepo.fetchHideTargets().cache()
Single.fromCallable { packageManager.getInstalledApplications(0) }
magiskRepo.fetchApps()
.flattenAsFlowable { it }
.filter { it.enabled && !blacklist.contains(it.packageName) }
.map {
val label = Utils.getAppLabel(it, packageManager)
val icon = it.loadIcon(packageManager)
HideAppInfo(it, label, icon)
}
.filter { it.processes.isNotEmpty() }
.map { HideRvItem(it, hideTargets.blockingGet()) }
.toList()
.map { it.sortWith(compareBy(
{it.isHiddenState.value}, {it.item.name}, {it.packageName})); it }
.map {
it.sortedWith(compareBy(
{ it.isHiddenState.value },
{ it.item.name.toLowerCase() },
{ it.packageName }
))
}
.doOnSuccess { allItems.update(it) }
.flatMap { queryRaw() }
.applyViewModel(this)
@@ -103,26 +90,9 @@ class HideViewModel(
.toList()
.map { it to items.calculateDiff(it) }
private fun toggleItem(item: HideProcessRvItem) {
val state = if (item.isHidden.value) "add" else "rm"
"magiskhide --%s %s %s".format(state, item.packageName, item.process)
.let { Shell.su(it) }
.toSingle()
.map { it.submit() }
.subscribeK()
}
companion object {
private val blacklist = listOf(
App.self.packageName,
"android",
"com.android.chrome",
"com.chrome.beta",
"com.chrome.dev",
"com.chrome.canary",
"com.android.webview",
"com.google.android.webview"
)
}
private fun toggleItem(item: HideProcessRvItem) = magiskRepo
.toggleHide(item.isHidden.value, item.packageName, item.process)
.subscribeK()
.add()
}

View File

@@ -28,7 +28,7 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
(findItem(R.id.app_search).actionView as? SearchView)
?.setOnQueryTextListener(this@MagiskHideFragment)
val showSystem = Config.get<Boolean>(Config.Key.SHOW_SYSTEM_APP)
val showSystem = Config.showSystemApp
findItem(R.id.show_system).isChecked = showSystem
viewModel.isShowSystem.value = showSystem
@@ -39,10 +39,8 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
if (item.itemId == R.id.show_system) {
val showSystem = !item.isChecked
item.isChecked = showSystem
Config.set(Config.Key.SHOW_SYSTEM_APP, showSystem)
Config.showSystemApp = showSystem
viewModel.isShowSystem.value = showSystem
//adapter!!.setShowSystem(showSystem)
//adapter!!.filter(search!!.query.toString())
}
return true
}
@@ -56,9 +54,4 @@ class MagiskHideFragment : MagiskFragment<HideViewModel, FragmentMagiskHideBindi
viewModel.query.value = query.orEmpty()
return false
}
/*override fun onEvent(event: Int) {
//mSwipeRefreshLayout!!.isRefreshing = false
adapter!!.filter(search!!.query.toString())
}*/
}

View File

@@ -1,16 +1,20 @@
package com.topjohnwu.magisk.ui.home
import android.app.Activity
import android.os.Bundle
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.viewevents.ViewEvent
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.databinding.FragmentMagiskBinding
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.ui.base.MagiskActivity
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.utils.ISafetyNetHelper
import com.topjohnwu.magisk.utils.copyTo
import com.topjohnwu.magisk.utils.inject
import com.topjohnwu.magisk.view.MarkDownWindow
import com.topjohnwu.magisk.view.dialogs.*
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import dalvik.system.DexClassLoader
import org.koin.androidx.viewmodel.ext.android.viewModel
@@ -21,6 +25,8 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
override val layoutRes: Int = R.layout.fragment_magisk
override val viewModel: HomeViewModel by viewModel()
val magiskRepo: MagiskRepository by inject()
lateinit var EXT_FILE: File
override fun onResponse(responseCode: Int) = viewModel.finishSafetyNetCheck(responseCode)
@@ -37,6 +43,11 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EXT_FILE = File("${requireActivity().filesDir.parent}/snet", "snet.apk")
}
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
@@ -45,7 +56,7 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
private fun installMagisk() {
// Show Manager update first
if (Config.remoteManagerVersionCode > BuildConfig.VERSION_CODE) {
if (Info.remote.app.versionCode > BuildConfig.VERSION_CODE) {
installManager()
return
}
@@ -61,9 +72,9 @@ class HomeFragment : MagiskFragment<HomeViewModel, FragmentMagiskBinding>(),
.show(requireActivity(), null, resources.openRawResource(R.raw.changelog))
private fun downloadSafetyNet(requiresUserInput: Boolean = true) {
fun download() = Networking
.get(Const.Url.SNET_URL)
.getAsFile(EXT_APK) { updateSafetyNet(true) }
fun download() = magiskRepo.fetchSafetynet()
.map { it.byteStream().copyTo(EXT_FILE) }
.subscribeK { updateSafetyNet(true) }
if (!requiresUserInput) {
download()

View File

@@ -1,29 +1,29 @@
package com.topjohnwu.magisk.ui.home
import android.content.Context
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSubscribeUi
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.*
import com.topjohnwu.magisk.data.repository.MagiskRepository
import com.topjohnwu.magisk.model.events.*
import com.topjohnwu.magisk.model.observer.Observer
import com.topjohnwu.magisk.tasks.CheckUpdates
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.net.Networking
import com.topjohnwu.magisk.utils.ISafetyNetHelper
import com.topjohnwu.magisk.utils.packageName
import com.topjohnwu.magisk.utils.res
import com.topjohnwu.magisk.utils.toggle
import com.topjohnwu.superuser.Shell
class HomeViewModel(
private val context: Context
private val magiskRepo: MagiskRepository
) : MagiskViewModel() {
val isAdvancedExpanded = KObservableField(false)
val isForceEncryption = KObservableField(Config.keepEnc)
val isKeepVerity = KObservableField(Config.keepVerity)
val isForceEncryption = KObservableField(Info.keepEnc)
val isKeepVerity = KObservableField(Info.keepVerity)
val magiskState = KObservableField(MagiskState.LOADING)
val magiskStateText = Observer(magiskState) {
@@ -38,7 +38,7 @@ class HomeViewModel(
val magiskCurrentVersion = KObservableField("")
val magiskLatestVersion = KObservableField("")
val magiskAdditionalInfo = Observer(magiskState) {
if (Config.get<Boolean>(Config.Key.COREONLY))
if (Config.coreOnly)
R.string.core_only_enabled.res()
else
""
@@ -83,25 +83,16 @@ class HomeViewModel(
private var shownDialog = false
init {
Event.register(this)
isForceEncryption.addOnPropertyChangedCallback {
Config.keepEnc = it ?: return@addOnPropertyChangedCallback
Info.keepEnc = it ?: return@addOnPropertyChangedCallback
}
isKeepVerity.addOnPropertyChangedCallback {
Config.keepVerity = it ?: return@addOnPropertyChangedCallback
Info.keepVerity = it ?: return@addOnPropertyChangedCallback
}
refresh()
}
override fun onEvent(event: Int) {
updateSelf()
ensureEnv()
}
override fun getListeningEvents(): IntArray = intArrayOf(Event.UPDATE_CHECK_DONE)
fun paypalPressed() = OpenLinkEvent(Const.Url.PAYPAL_URL).publish()
fun patreonPressed() = OpenLinkEvent(Const.Url.PATREON_URL).publish()
fun twitterPressed() = OpenLinkEvent(Const.Url.TWITTER_URL).publish()
@@ -160,43 +151,41 @@ class HomeViewModel(
}
fun refresh() {
state = State.LOADING
magiskState.value = MagiskState.LOADING
managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text
Event.reset(this)
Config.remoteMagiskVersionString = null
Config.remoteMagiskVersionCode = -1
magiskRepo.fetchUpdate()
.applyViewModel(this)
.doOnSubscribeUi {
magiskState.value = MagiskState.LOADING
managerState.value = MagiskState.LOADING
ctsState.value = SafetyNetState.IDLE
basicIntegrityState.value = SafetyNetState.IDLE
safetyNetTitle.value = R.string.safetyNet_check_text
}
.subscribeK {
updateSelf()
ensureEnv()
}
hasRoot.value = Shell.rootAccess()
if (Networking.checkNetworkStatus(context)) {
CheckUpdates.check()
} else {
state = State.LOADING_FAILED
}
}
private fun updateSelf() {
state = State.LOADED
magiskState.value = when (Config.magiskVersionCode) {
magiskState.value = when (Info.magiskVersionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED
!in Config.remoteMagiskVersionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
!in Info.remote.magisk.versionCode..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
}
magiskCurrentVersion.value = if (magiskState.value != MagiskState.NOT_INSTALLED) {
version.format(Config.magiskVersionString, Config.magiskVersionCode)
version.format(Info.magiskVersionString, Info.magiskVersionCode)
} else {
""
}
magiskLatestVersion.value = version
.format(Config.remoteMagiskVersionString, Config.remoteMagiskVersionCode)
.format(Info.remote.magisk.version, Info.remote.magisk.versionCode)
managerState.value = when (Config.remoteManagerVersionCode) {
managerState.value = when (Info.remote.app.versionCode) {
in Int.MIN_VALUE until 0 -> MagiskState.NOT_INSTALLED //wrong update channel
in (BuildConfig.VERSION_CODE + 1)..Int.MAX_VALUE -> MagiskState.OBSOLETE
else -> MagiskState.UP_TO_DATE
@@ -206,7 +195,7 @@ class HomeViewModel(
.format(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE)
managerLatestVersion.value = version
.format(Config.remoteManagerVersionString, Config.remoteManagerVersionCode)
.format(Info.remote.app.version, Info.remote.app.versionCode)
}
private fun ensureEnv() {

View File

@@ -1,10 +1,9 @@
package com.topjohnwu.magisk.ui.log
import android.content.res.Resources
import androidx.databinding.ObservableArrayList
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSuccessUi
import com.skoumal.teanity.extensions.doOnSubscribeUi
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
@@ -12,14 +11,15 @@ import com.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.model.entity.recycler.*
import com.topjohnwu.magisk.data.repository.LogRepository
import com.topjohnwu.magisk.model.binding.BindingAdapter
import com.topjohnwu.magisk.model.entity.recycler.ConsoleRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogItemRvItem
import com.topjohnwu.magisk.model.entity.recycler.LogRvItem
import com.topjohnwu.magisk.model.entity.recycler.MagiskLogRvItem
import com.topjohnwu.magisk.model.events.PageChangedEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.zip
import com.topjohnwu.superuser.Shell
import io.reactivex.Single
import me.tatarka.bindingcollectionadapter2.BindingViewPagerAdapter
import me.tatarka.bindingcollectionadapter2.OnItemBind
import java.io.File
@@ -28,9 +28,10 @@ import java.util.*
class LogViewModel(
private val resources: Resources,
private val database: MagiskDB
private val logRepo: LogRepository
) : MagiskViewModel(), BindingViewPagerAdapter.PageTitles<ComparableRvItem<*>> {
val itemsAdapter = BindingAdapter()
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = OnItemBind<ComparableRvItem<*>> { itemBinding, _, item ->
item.bind(itemBinding)
@@ -42,6 +43,8 @@ class LogViewModel(
private val logItem get() = items[0] as LogRvItem
private val magiskLogItem get() = items[1] as MagiskLogRvItem
val scrollPosition = KObservableField(0)
init {
currentPage.addOnPropertyChangedCallback {
it ?: return@addOnPropertyChangedCallback
@@ -58,9 +61,14 @@ class LogViewModel(
else -> ""
}
fun refresh() = zip(updateLogs(), updateMagiskLog()) { _, _ -> true }
.subscribeK()
.add()
fun scrollDownPressed() {
scrollPosition.value = magiskLogItem.items.size - 1
}
fun refresh() {
fetchLogs().subscribeK { logItem.update(it) }
fetchMagiskLog().subscribeK { magiskLogItem.update(it) }
}
fun saveLog() {
val now = Calendar.getInstance()
@@ -88,35 +96,25 @@ class LogViewModel(
else -> Unit
}
private fun clearLogs(callback: () -> Unit) {
Single.fromCallable { database.clearLogs() }
.subscribeK {
SnackbarEvent(R.string.logs_cleared).publish()
callback()
}
.add()
}
private fun clearLogs(callback: () -> Unit) = logRepo.clearLogs()
.doOnSubscribeUi(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
.add()
private fun clearMagiskLogs(callback: () -> Unit) {
Shell.su("echo -n > " + Const.MAGISK_LOG).submit {
SnackbarEvent(R.string.logs_cleared).publish()
callback()
}
}
private fun clearMagiskLogs(callback: () -> Unit) = logRepo.clearMagiskLogs()
.ignoreElement()
.doOnComplete(callback)
.subscribeK { SnackbarEvent(R.string.logs_cleared).publish() }
.add()
private fun updateLogs() = Single.fromCallable { database.logs }
private fun fetchLogs() = logRepo.fetchLogs()
.flattenAsFlowable { it }
.map { it.map { LogItemEntryRvItem(it) } }
.map { LogItemRvItem(ObservableArrayList<ComparableRvItem<*>>().apply { addAll(it) }) }
.map { LogItemRvItem(it) }
.toList()
.doOnSuccessUi { logItem.update(it) }
private fun updateMagiskLog() = Shell.su("tail -n 5000 ${Const.MAGISK_LOG}").toSingle()
.map { it.exec() }
.map { it.out }
private fun fetchMagiskLog() = logRepo.fetchMagiskLogs()
.flattenAsFlowable { it }
.map { ConsoleRvItem(it) }
.toList()
.doOnSuccessUi { magiskLogItem.update(it) }
}

View File

@@ -2,16 +2,15 @@ package com.topjohnwu.magisk.ui.module
import android.content.res.Resources
import android.database.Cursor
import androidx.annotation.StringRes
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.extensions.doOnSuccessUi
import com.skoumal.teanity.extensions.subscribeK
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.model.entity.Module
import com.topjohnwu.magisk.model.entity.Repo
import com.topjohnwu.magisk.model.entity.recycler.ModuleRvItem
import com.topjohnwu.magisk.model.entity.recycler.RepoRvItem
@@ -21,7 +20,6 @@ import com.topjohnwu.magisk.model.events.OpenChangelogEvent
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.tasks.UpdateRepos
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.Event
import com.topjohnwu.magisk.utils.Utils
import com.topjohnwu.magisk.utils.toSingle
import com.topjohnwu.magisk.utils.update
@@ -30,8 +28,9 @@ import io.reactivex.disposables.Disposable
import me.tatarka.bindingcollectionadapter2.OnItemBind
class ModuleViewModel(
private val repoDatabase: RepoDatabaseHelper,
private val resources: Resources
private val resources: Resources,
private val repoDatabase: RepoDatabaseHelper,
private val repoUpdater: UpdateRepos
) : MagiskViewModel() {
val query = KObservableField("")
@@ -52,36 +51,23 @@ class ModuleViewModel(
queryDisposable?.dispose()
queryDisposable = query()
}
Event.register(this)
refresh()
}
override fun getListeningEvents(): IntArray {
return intArrayOf(Event.MODULE_LOAD_DONE, Event.REPO_LOAD_DONE)
}
override fun onEvent(event: Int) = when (event) {
Event.MODULE_LOAD_DONE -> updateModules(Event.getResult(event))
Event.REPO_LOAD_DONE -> updateRepos()
else -> Unit
refresh(false)
}
fun fabPressed() = OpenFilePickerEvent().publish()
fun repoPressed(item: RepoRvItem) = OpenChangelogEvent(item.item).publish()
fun downloadPressed(item: RepoRvItem) = InstallModuleEvent(item.item).publish()
fun refresh() {
state = State.LOADING
Utils.loadModules(true)
UpdateRepos().exec(true)
}
private fun updateModules(result: Map<String, Module>) = result.values
.map { ModuleRvItem(it) }
.let { itemsInstalled.update(it) }
internal fun updateRepos() {
Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } }
fun refresh(force: Boolean) {
Single.fromCallable { Utils.loadModulesLeanback() }
.map { it.values.toList() }
.flattenAsFlowable { it }
.map { ModuleRvItem(it) }
.toList()
.map { it to itemsInstalled.calculateDiff(it) }
.doOnSuccessUi { itemsInstalled.update(it.first, it.second) }
.flatMap { repoUpdater.exec(force) }
.flatMap { Single.fromCallable { repoDatabase.repoCursor.toList { Repo(it) } } }
.flattenAsFlowable { it }
.map { RepoRvItem(it) }
.toList()
@@ -89,7 +75,6 @@ class ModuleViewModel(
.flatMap { queryRaw() }
.applyViewModel(this)
.subscribeK { itemsRemote.update(it.first, it.second) }
.add()
}
private fun query() = queryRaw()
@@ -109,27 +94,20 @@ class ModuleViewModel(
private fun List<RepoRvItem>.divide(): List<ComparableRvItem<*>> {
val installed = itemsInstalled.filterIsInstance<ModuleRvItem>()
val installedModules = filter { installed.any { item -> it.item.id == item.item.id } }
fun installedByID(id: String) = installed.firstOrNull { it.item.id == id }
fun <T : ComparableRvItem<*>> List<T>.withTitle(text: Int) =
if (isEmpty()) this else listOf(SectionRvItem(resources.getString(text))) + this
fun List<RepoRvItem>.filterObsolete() = filter {
val module = installedByID(it.item.id) ?: return@filter false
module.item.versionCode != it.item.versionCode
val groupedItems = groupBy { repo ->
installed.firstOrNull { it.item.id == repo.item.id }?.let {
if (it.item.versionCode < repo.item.versionCode) MODULE_UPDATABLE
else MODULE_INSTALLED
} ?: MODULE_REMOTE
}
val resultObsolete = installedModules.filterObsolete()
val resultInstalled = installedModules - resultObsolete
val resultRemote = toList() - installedModules
fun buildList(@StringRes text: Int, list: List<RepoRvItem>): List<ComparableRvItem<*>> {
return if (list.isEmpty()) list
else listOf(SectionRvItem(resources.getString(text))) + list
}
return buildList(R.string.update_available, resultObsolete) +
buildList(R.string.installed, resultInstalled) +
buildList(R.string.not_installed, resultRemote)
return groupedItems.getOrElse(MODULE_UPDATABLE) { listOf() }.withTitle(R.string.update_available) +
groupedItems.getOrElse(MODULE_INSTALLED) { listOf() }.withTitle(R.string.installed) +
groupedItems.getOrElse(MODULE_REMOTE) { listOf() }.withTitle(R.string.not_installed)
}
private fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
@@ -138,4 +116,10 @@ class ModuleViewModel(
return out
}
companion object {
protected const val MODULE_INSTALLED = 0
protected const val MODULE_REMOTE = 1
protected const val MODULE_UPDATABLE = 2
}
}

View File

@@ -16,7 +16,7 @@ import com.topjohnwu.magisk.databinding.FragmentModulesBinding
import com.topjohnwu.magisk.model.events.OpenFilePickerEvent
import com.topjohnwu.magisk.ui.base.MagiskFragment
import com.topjohnwu.magisk.ui.flash.FlashActivity
import com.topjohnwu.magisk.utils.RootUtils
import com.topjohnwu.magisk.utils.reboot
import com.topjohnwu.superuser.Shell
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@@ -28,7 +28,7 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == Const.ID.FETCH_ZIP && resultCode == Activity.RESULT_OK && data != null) {
// Get the URI of the selected file
val intent = Intent(activity, ClassMap.get<Any>(FlashActivity::class.java))
val intent = Intent(activity, ClassMap[FlashActivity::class.java])
intent.setData(data.data).putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP)
startActivity(intent)
}
@@ -64,19 +64,20 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.reboot -> {
RootUtils.reboot()
reboot()
return true
}
R.id.reboot_recovery -> {
Shell.su("/system/bin/reboot recovery").submit()
reboot("recovery")
return true
}
R.id.reboot_bootloader -> {
reboot("booloader")
Shell.su("/system/bin/reboot bootloader").submit()
return true
}
R.id.reboot_download -> {
Shell.su("/system/bin/reboot download").submit()
reboot("download")
return true
}
else -> return false
@@ -84,32 +85,12 @@ class ModulesFragment : MagiskFragment<ModuleViewModel, FragmentModulesBinding>(
}
private fun selectFile() {
magiskActivity.runWithExternalRW {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
magiskActivity.withExternalRW {
onSuccess {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "application/zip"
startActivityForResult(intent, Const.ID.FETCH_ZIP)
}
}
}
/*override fun getListeningEvents(): IntArray {
return intArrayOf(Event.MODULE_LOAD_DONE)
}
override fun onEvent(event: Int) {
updateUI(Event.getResult(event))
}*/
/*private fun updateUI(moduleMap: Map<String, Module>) {
listModules.clear()
listModules.addAll(moduleMap.values)
if (listModules.size == 0) {
emptyRv!!.visibility = View.VISIBLE
recyclerView!!.visibility = View.GONE
} else {
emptyRv!!.visibility = View.GONE
recyclerView!!.visibility = View.VISIBLE
recyclerView!!.adapter = ModulesAdapter(listModules)
}
mSwipeRefreshLayout!!.isRefreshing = false
}*/
}

View File

@@ -53,10 +53,10 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
.setTitle(R.string.sorting_order)
.setSingleChoiceItems(
R.array.sorting_orders,
Config.get<Int>(Config.Key.REPO_ORDER)!!
Config.repoOrder
) { d, which ->
Config.set(Config.Key.REPO_ORDER, which)
viewModel.updateRepos()
Config.repoOrder = which
viewModel.refresh(false)
d.dismiss()
}.show()
}
@@ -74,20 +74,22 @@ class ReposFragment : MagiskFragment<ModuleViewModel, FragmentReposBinding>(),
}
private fun openChangelog(item: Repo) {
MarkDownWindow.show(context, null, item.detailUrl)
MarkDownWindow.show(requireActivity(), null, item.detailUrl)
}
private fun installModule(item: Repo) {
val context = magiskActivity
fun download(install: Boolean) {
context.runWithExternalRW {
val intent = Intent(activity, ClassMap.get<Any>(DownloadModuleService::class.java))
.putExtra("repo", item).putExtra("install", install)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent) //hmm, service starts itself in foreground, this seems unnecessary
} else {
context.startService(intent)
context.withExternalRW {
onSuccess {
val intent = Intent(activity, ClassMap[DownloadModuleService::class.java])
.putExtra("repo", item).putExtra("install", install)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
}
}

View File

@@ -1,300 +0,0 @@
package com.topjohnwu.magisk.ui.settings;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.tasks.CheckUpdates;
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment;
import com.topjohnwu.magisk.utils.DownloadApp;
import com.topjohnwu.magisk.utils.Event;
import com.topjohnwu.magisk.utils.FingerprintHelper;
import com.topjohnwu.magisk.utils.LocaleManager;
import com.topjohnwu.magisk.utils.PatchAPK;
import com.topjohnwu.magisk.utils.Utils;
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog;
import com.topjohnwu.net.Networking;
import com.topjohnwu.superuser.Shell;
import java.io.IOException;
import java.util.Arrays;
import java.util.Locale;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreferenceCompat;
public class SettingsFragment extends BasePreferenceFragment {
private ListPreference updateChannel, autoRes, suNotification,
requestTimeout, rootConfig, multiuserConfig, nsConfig;
@Override
public void onStart() {
super.onStart();
setHasOptionsMenu(true);
requireActivity().setTitle(R.string.settings);
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
getPreferenceManager().setStorageDeviceProtected();
setPreferencesFromResource(R.xml.app_settings, rootKey);
boolean showSuperuser = Utils.showSuperUser();
app.getPrefs().edit()
.putBoolean(Config.Key.SU_FINGERPRINT, FingerprintHelper.useFingerprint())
.apply();
PreferenceScreen prefScreen = getPreferenceScreen();
PreferenceCategory generalCatagory = (PreferenceCategory) findPreference("general");
PreferenceCategory magiskCategory = (PreferenceCategory) findPreference("magisk");
PreferenceCategory suCategory = (PreferenceCategory) findPreference("superuser");
Preference hideManager = findPreference("hide");
hideManager.setOnPreferenceClickListener(pref -> {
PatchAPK.hideManager();
return true;
});
Preference restoreManager = findPreference("restore");
restoreManager.setOnPreferenceClickListener(pref -> {
DownloadApp.restore();
return true;
});
findPreference("clear").setOnPreferenceClickListener(pref -> {
app.getPrefs().edit().remove(Config.Key.ETAG_KEY).apply();
app.getRepoDB().clearRepo();
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT);
return true;
});
findPreference("hosts").setOnPreferenceClickListener(pref -> {
Shell.su("add_hosts_module").exec();
Utils.loadModules();
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT);
return true;
});
updateChannel = (ListPreference) findPreference(Config.Key.UPDATE_CHANNEL);
rootConfig = (ListPreference) findPreference(Config.Key.ROOT_ACCESS);
autoRes = (ListPreference) findPreference(Config.Key.SU_AUTO_RESPONSE);
requestTimeout = (ListPreference) findPreference(Config.Key.SU_REQUEST_TIMEOUT);
suNotification = (ListPreference) findPreference(Config.Key.SU_NOTIFICATION);
multiuserConfig = (ListPreference) findPreference(Config.Key.SU_MULTIUSER_MODE);
nsConfig = (ListPreference) findPreference(Config.Key.SU_MNT_NS);
SwitchPreferenceCompat reauth = (SwitchPreferenceCompat) findPreference(Config.Key.SU_REAUTH);
SwitchPreferenceCompat fingerprint = (SwitchPreferenceCompat) findPreference(Config.Key.SU_FINGERPRINT);
updateChannel.setOnPreferenceChangeListener((p, o) -> {
int prev = Config.get(Config.Key.UPDATE_CHANNEL);
int channel = Integer.parseInt((String) o);
if (channel == Config.Value.CUSTOM_CHANNEL) {
View v = LayoutInflater.from(requireActivity()).inflate(R.layout.custom_channel_dialog, null);
EditText url = v.findViewById(R.id.custom_url);
url.setText(app.getPrefs().getString(Config.Key.CUSTOM_CHANNEL, ""));
new AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.setPositiveButton(R.string.ok, (d, i) ->
Config.set(Config.Key.CUSTOM_CHANNEL, url.getText().toString()))
.setNegativeButton(R.string.close, (d, i) ->
Config.set(Config.Key.UPDATE_CHANNEL, prev))
.setOnCancelListener(d ->
Config.set(Config.Key.UPDATE_CHANNEL, prev))
.show();
}
return true;
});
/* We only show canary channels if user is already on canary channel
* or the user have already chosen canary channel */
if (!Utils.isCanary() &&
(int) Config.get(Config.Key.UPDATE_CHANNEL) < Config.Value.CANARY_CHANNEL) {
// Remove the last 2 entries
CharSequence[] entries = updateChannel.getEntries();
updateChannel.setEntries(Arrays.copyOf(entries, entries.length - 2));
}
setSummary();
// Disable dangerous settings in secondary user
if (Const.USER_ID > 0) {
suCategory.removePreference(multiuserConfig);
}
// Disable re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
reauth.setEnabled(false);
reauth.setChecked(false);
reauth.setSummary(R.string.android_o_not_support);
}
// Disable fingerprint option if not possible
if (!FingerprintHelper.canUseFingerprint()) {
fingerprint.setEnabled(false);
fingerprint.setChecked(false);
fingerprint.setSummary(R.string.disable_fingerprint);
}
if (Shell.rootAccess() && Const.USER_ID == 0) {
if (app.getPackageName().equals(BuildConfig.APPLICATION_ID)) {
generalCatagory.removePreference(restoreManager);
} else {
if (!Networking.checkNetworkStatus(app))
generalCatagory.removePreference(restoreManager);
generalCatagory.removePreference(hideManager);
}
} else {
generalCatagory.removePreference(restoreManager);
generalCatagory.removePreference(hideManager);
}
if (!showSuperuser) {
prefScreen.removePreference(suCategory);
}
if (!Shell.rootAccess()) {
prefScreen.removePreference(magiskCategory);
generalCatagory.removePreference(hideManager);
}
}
private void setLocalePreference(ListPreference lp) {
CharSequence[] entries = new CharSequence[LocaleManager.locales.size() + 1];
CharSequence[] entryValues = new CharSequence[LocaleManager.locales.size() + 1];
entries[0] = LocaleManager.getString(LocaleManager.defaultLocale, R.string.system_default);
entryValues[0] = "";
int i = 1;
for (Locale locale : LocaleManager.locales) {
entries[i] = locale.getDisplayName(locale);
entryValues[i++] = LocaleManager.toLanguageTag(locale);
}
lp.setEntries(entries);
lp.setEntryValues(entryValues);
lp.setSummary(LocaleManager.locale.getDisplayName(LocaleManager.locale));
}
@Override
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
switch (key) {
case Config.Key.ROOT_ACCESS:
case Config.Key.SU_MULTIUSER_MODE:
case Config.Key.SU_MNT_NS:
app.getDB().setSettings(key, Utils.getPrefsInt(prefs, key));
break;
case Config.Key.DARK_THEME:
requireActivity().recreate();
break;
case Config.Key.COREONLY:
if (prefs.getBoolean(key, false)) {
try {
Const.MAGISK_DISABLE_FILE.createNewFile();
} catch (IOException ignored) {}
} else {
Const.MAGISK_DISABLE_FILE.delete();
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG);
break;
case Config.Key.MAGISKHIDE:
if (prefs.getBoolean(key, false)) {
Shell.su("magiskhide --enable").submit();
} else {
Shell.su("magiskhide --disable").submit();
}
break;
case Config.Key.LOCALE:
LocaleManager.setLocale(app);
requireActivity().recreate();
break;
case Config.Key.UPDATE_CHANNEL:
case Config.Key.CUSTOM_CHANNEL:
CheckUpdates.check();
break;
case Config.Key.CHECK_UPDATES:
Utils.scheduleUpdateCheck();
break;
}
setSummary(key);
}
@Override
public boolean onPreferenceTreeClick(Preference preference) {
String key = preference.getKey();
switch (key) {
case Config.Key.SU_FINGERPRINT:
boolean checked = ((SwitchPreferenceCompat) preference).isChecked();
((SwitchPreferenceCompat) preference).setChecked(!checked);
new FingerprintAuthDialog(requireActivity(), () -> {
((SwitchPreferenceCompat) preference).setChecked(checked);
Config.set(key, checked);
}).show();
break;
}
return true;
}
private void setSummary(String key) {
switch (key) {
case Config.Key.UPDATE_CHANNEL:
int ch = Config.get(key);
ch = ch < 0 ? Config.Value.STABLE_CHANNEL : ch;
updateChannel.setSummary(getResources()
.getStringArray(R.array.update_channel)[ch]);
break;
case Config.Key.ROOT_ACCESS:
rootConfig.setSummary(getResources()
.getStringArray(R.array.su_access)[(int)Config.get(key)]);
break;
case Config.Key.SU_AUTO_RESPONSE:
autoRes.setSummary(getResources()
.getStringArray(R.array.auto_response)[(int)Config.get(key)]);
break;
case Config.Key.SU_NOTIFICATION:
suNotification.setSummary(getResources()
.getStringArray(R.array.su_notification)[(int)Config.get(key)]);
break;
case Config.Key.SU_REQUEST_TIMEOUT:
requestTimeout.setSummary(
getString(R.string.request_timeout_summary, (int)Config.get(key)));
break;
case Config.Key.SU_MULTIUSER_MODE:
multiuserConfig.setSummary(getResources()
.getStringArray(R.array.multiuser_summary)[(int)Config.get(key)]);
break;
case Config.Key.SU_MNT_NS:
nsConfig.setSummary(getResources()
.getStringArray(R.array.namespace_summary)[(int)Config.get(key)]);
break;
}
}
private void setSummary() {
setSummary(Config.Key.UPDATE_CHANNEL);
setSummary(Config.Key.ROOT_ACCESS);
setSummary(Config.Key.SU_AUTO_RESPONSE);
setSummary(Config.Key.SU_NOTIFICATION);
setSummary(Config.Key.SU_REQUEST_TIMEOUT);
setSummary(Config.Key.SU_MULTIUSER_MODE);
setSummary(Config.Key.SU_MNT_NS);
}
@Override
public void onEvent(int event) {
setLocalePreference((ListPreference) findPreference(Config.Key.LOCALE));
}
@Override
public int[] getListeningEvents() {
return new int[] {Event.LOCALE_FETCH_DONE};
}
}

View File

@@ -0,0 +1,265 @@
package com.topjohnwu.magisk.ui.settings
import android.content.SharedPreferences
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.edit
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.SwitchPreferenceCompat
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.Const
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.RepoDatabaseHelper
import com.topjohnwu.magisk.ui.base.BasePreferenceFragment
import com.topjohnwu.magisk.utils.*
import com.topjohnwu.magisk.view.dialogs.FingerprintAuthDialog
import com.topjohnwu.net.Networking
import com.topjohnwu.superuser.Shell
import org.koin.android.ext.android.inject
class SettingsFragment : BasePreferenceFragment() {
private val repoDatabase: RepoDatabaseHelper by inject()
private lateinit var updateChannel: ListPreference
private lateinit var autoRes: ListPreference
private lateinit var suNotification: ListPreference
private lateinit var requestTimeout: ListPreference
private lateinit var rootConfig: ListPreference
private lateinit var multiuserConfig: ListPreference
private lateinit var nsConfig: ListPreference
override fun onStart() {
super.onStart()
setHasOptionsMenu(true)
requireActivity().setTitle(R.string.settings)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.setStorageDeviceProtected()
setPreferencesFromResource(R.xml.app_settings, rootKey)
updateChannel = findPref(Config.Key.UPDATE_CHANNEL)
rootConfig = findPref(Config.Key.ROOT_ACCESS)
autoRes = findPref(Config.Key.SU_AUTO_RESPONSE)
requestTimeout = findPref(Config.Key.SU_REQUEST_TIMEOUT)
suNotification = findPref(Config.Key.SU_NOTIFICATION)
multiuserConfig = findPref(Config.Key.SU_MULTIUSER_MODE)
nsConfig = findPref(Config.Key.SU_MNT_NS)
val reauth = findPreference(Config.Key.SU_REAUTH) as SwitchPreferenceCompat
val fingerprint = findPreference(Config.Key.SU_FINGERPRINT) as SwitchPreferenceCompat
val generalCatagory = findPreference("general") as PreferenceCategory
val magiskCategory = findPreference("magisk") as PreferenceCategory
val suCategory = findPreference("superuser") as PreferenceCategory
val hideManager = findPreference("hide")
hideManager.setOnPreferenceClickListener {
PatchAPK.hideManager()
true
}
val restoreManager = findPreference("restore")
restoreManager.setOnPreferenceClickListener {
DownloadApp.restore()
true
}
findPreference("clear").setOnPreferenceClickListener {
prefs.edit {
remove(Config.Key.ETAG_KEY)
}
repoDatabase.clearRepo()
Utils.toast(R.string.repo_cache_cleared, Toast.LENGTH_SHORT)
true
}
findPreference("hosts").setOnPreferenceClickListener {
Shell.su("add_hosts_module").exec()
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
true
}
updateChannel.setOnPreferenceChangeListener { _, value ->
val channel = Integer.parseInt(value as String)
val previous = Config.updateChannel
if (channel == Config.Value.CUSTOM_CHANNEL) {
val v = LayoutInflater.from(requireActivity())
.inflate(R.layout.custom_channel_dialog, null)
val url = v.findViewById<EditText>(R.id.custom_url)
url.setText(Config.customChannelUrl)
AlertDialog.Builder(requireActivity())
.setTitle(R.string.settings_update_custom)
.setView(v)
.setPositiveButton(R.string.ok) { _, _ ->
Config.customChannelUrl = url.text.toString() }
.setNegativeButton(R.string.close) { _, _ ->
Config.updateChannel = previous }
.setOnCancelListener { Config.updateChannel = previous }
.show()
}
true
}
setLocalePreference(findPreference(Config.Key.LOCALE) as ListPreference)
/* We only show canary channels if user is already on canary channel
* or the user have already chosen canary channel */
if (!Utils.isCanary && Config.updateChannel < Config.Value.CANARY_CHANNEL) {
// Remove the last 2 entries
val entries = updateChannel.entries
updateChannel.entries = entries.copyOf(entries.size - 2)
}
setSummary()
// Disable dangerous settings in secondary user
if (Const.USER_ID > 0) {
suCategory.removePreference(multiuserConfig)
}
// Disable re-authentication option on Android O, it will not work
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
reauth.isEnabled = false
reauth.isChecked = false
reauth.setSummary(R.string.android_o_not_support)
}
// Disable fingerprint option if not possible
if (!FingerprintHelper.canUseFingerprint()) {
fingerprint.isEnabled = false
fingerprint.isChecked = false
fingerprint.setSummary(R.string.disable_fingerprint)
}
if (Shell.rootAccess() && Const.USER_ID == 0) {
if (app.packageName == BuildConfig.APPLICATION_ID) {
generalCatagory.removePreference(restoreManager)
} else {
if (!Networking.checkNetworkStatus(requireContext())) {
generalCatagory.removePreference(restoreManager)
}
generalCatagory.removePreference(hideManager)
}
} else {
generalCatagory.removePreference(restoreManager)
generalCatagory.removePreference(hideManager)
}
if (!Utils.showSuperUser()) {
preferenceScreen.removePreference(suCategory)
}
if (!Shell.rootAccess()) {
preferenceScreen.removePreference(magiskCategory)
generalCatagory.removePreference(hideManager)
}
}
override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String) {
when (key) {
Config.Key.ROOT_ACCESS -> Config.rootMode = Utils.getPrefsInt(prefs, key)
Config.Key.SU_MULTIUSER_MODE -> Config.suMultiuserMode = Utils.getPrefsInt(prefs, key)
Config.Key.SU_MNT_NS -> Config.suMntNamespaceMode = Utils.getPrefsInt(prefs, key)
Config.Key.DARK_THEME -> requireActivity().recreate()
Config.Key.COREONLY -> {
if (prefs.getBoolean(key, false)) {
runCatching {
Const.MAGISK_DISABLE_FILE.createNewFile()
}
} else {
Const.MAGISK_DISABLE_FILE.delete()
}
Utils.toast(R.string.settings_reboot_toast, Toast.LENGTH_LONG)
}
Config.Key.MAGISKHIDE -> if (prefs.getBoolean(key, false)) {
Shell.su("magiskhide --enable").submit()
} else {
Shell.su("magiskhide --disable").submit()
}
Config.Key.LOCALE -> {
LocaleManager.setLocale(app)
requireActivity().recreate()
}
Config.Key.CHECK_UPDATES -> Utils.scheduleUpdateCheck()
}
setSummary(key)
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
when (preference.key) {
Config.Key.SU_FINGERPRINT -> {
val checked = (preference as SwitchPreferenceCompat).isChecked
preference.isChecked = !checked
FingerprintAuthDialog(requireActivity()) {
preference.isChecked = checked
Config.suFingerprint = checked
}.show()
}
}
return true
}
private fun setLocalePreference(lp: ListPreference) {
lp.isEnabled = false
LocaleManager.availableLocales
.map {
val names = mutableListOf<String>()
val values = mutableListOf<String>()
names.add(LocaleManager.getString(
LocaleManager.defaultLocale, R.string.system_default))
values.add("")
it.forEach { locale ->
names.add(locale.getDisplayName(locale))
values.add(LocaleManager.toLanguageTag(locale))
}
Pair(names.toTypedArray(), values.toTypedArray())
}.subscribeK { (names, values) ->
lp.isEnabled = true
lp.entries = names
lp.entryValues = values
lp.summary = LocaleManager.locale.getDisplayName(LocaleManager.locale)
}
}
private fun setSummary(key: String) {
when (key) {
Config.Key.ROOT_ACCESS -> rootConfig.summary = resources
.getStringArray(R.array.su_access)[Config.rootMode]
Config.Key.SU_MULTIUSER_MODE -> multiuserConfig.summary = resources
.getStringArray(R.array.multiuser_summary)[Config.suMultiuserMode]
Config.Key.SU_MNT_NS -> nsConfig.summary = resources
.getStringArray(R.array.namespace_summary)[Config.suMntNamespaceMode]
Config.Key.UPDATE_CHANNEL -> {
var ch = Config.updateChannel
ch = if (ch < 0) Config.Value.STABLE_CHANNEL else ch
updateChannel.summary = resources
.getStringArray(R.array.update_channel)[ch]
}
Config.Key.SU_AUTO_RESPONSE -> autoRes.summary = resources
.getStringArray(R.array.auto_response)[Config.suAutoReponse]
Config.Key.SU_NOTIFICATION -> suNotification.summary = resources
.getStringArray(R.array.su_notification)[Config.suNotification]
Config.Key.SU_REQUEST_TIMEOUT -> requestTimeout.summary =
getString(R.string.request_timeout_summary, Config.suDefaultTimeout)
}
}
private fun setSummary() {
setSummary(Config.Key.ROOT_ACCESS)
setSummary(Config.Key.SU_MULTIUSER_MODE)
setSummary(Config.Key.SU_MNT_NS)
setSummary(Config.Key.UPDATE_CHANNEL)
setSummary(Config.Key.SU_AUTO_RESPONSE)
setSummary(Config.Key.SU_NOTIFICATION)
setSummary(Config.Key.SU_REQUEST_TIMEOUT)
}
}

View File

@@ -10,7 +10,8 @@ import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.viewevents.SnackbarEvent
import com.topjohnwu.magisk.BR
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.entity.recycler.PolicyRvItem
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
@@ -24,7 +25,7 @@ import io.reactivex.Single
import me.tatarka.bindingcollectionadapter2.ItemBinding
class SuperuserViewModel(
private val database: MagiskDB,
private val appRepo: AppRepository,
private val packageManager: PackageManager,
private val resources: Resources,
rxBus: RxBus
@@ -50,13 +51,20 @@ class SuperuserViewModel(
}
fun updatePolicies() {
Single.fromCallable { database.policyList }
appRepo.fetchAll()
.flattenAsFlowable { it }
.map { PolicyRvItem(it, it.info.loadIcon(packageManager)) }
.map { PolicyRvItem(it, it.applicationInfo.loadIcon(packageManager)) }
.toList()
.map {
it.sortedWith(compareBy(
{ it.item.appName.toLowerCase() },
{ it.item.packageName }
))
}
.map { it to items.calculateDiff(it) }
.applySchedulers()
.applyViewModel(this)
.subscribeK { items.update(it) }
.subscribeK { items.update(it.first, it.second) }
.add()
}
@@ -85,28 +93,29 @@ class SuperuserViewModel(
private fun updatePolicy(it: PolicyUpdateEvent) = when (it) {
is PolicyUpdateEvent.Notification -> updatePolicy(it.item) {
val textId = if (it.logging) R.string.su_snack_notif_on else R.string.su_snack_notif_off
val textId =
if (it.notification) R.string.su_snack_notif_on else R.string.su_snack_notif_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
}
is PolicyUpdateEvent.Log -> updatePolicy(it.item) {
val textId =
if (it.notification) R.string.su_snack_log_on else R.string.su_snack_log_off
if (it.logging) R.string.su_snack_log_on else R.string.su_snack_log_off
val text = resources.getString(textId).format(it.appName)
SnackbarEvent(text).publish()
}
}
private fun updatePolicy(item: PolicyRvItem, onSuccess: (Policy) -> Unit) =
updatePolicy(item.item)
private fun updatePolicy(item: MagiskPolicy, onSuccess: (MagiskPolicy) -> Unit) =
updatePolicy(item)
.subscribeK { onSuccess(it) }
.add()
private fun togglePolicy(item: PolicyRvItem, enable: Boolean) {
fun updateState() {
item.item.policy = if (enable) Policy.ALLOW else Policy.DENY
val app = item.item.copy(policy = if (enable) MagiskPolicy.ALLOW else MagiskPolicy.DENY)
updatePolicy(item.item)
updatePolicy(app)
.map { it.policy == Policy.ALLOW }
.subscribeK {
val textId = if (it) R.string.su_snack_grant else R.string.su_snack_deny
@@ -128,12 +137,10 @@ class SuperuserViewModel(
}
}
private fun updatePolicy(policy: Policy) =
Single.fromCallable { database.updatePolicy(policy); policy }
.applySchedulers()
private fun updatePolicy(policy: MagiskPolicy) =
appRepo.update(policy).andThen(Single.just(policy))
private fun deletePolicy(policy: Policy) =
Single.fromCallable { database.deletePolicy(policy); policy }
.applySchedulers()
private fun deletePolicy(policy: MagiskPolicy) =
appRepo.delete(policy.uid).andThen(Single.just(policy))
}

View File

@@ -33,7 +33,7 @@ open class SuRequestActivity : MagiskActivity<SuRequestViewModel, ActivityReques
val action = intent.action
if (TextUtils.equals(action, GeneralReceiver.REQUEST)) {
if (!viewModel.handleRequest(intent) {})
if (!viewModel.handleRequest(intent))
finish()
return
}

View File

@@ -10,26 +10,31 @@ import android.hardware.fingerprint.FingerprintManager
import android.os.CountDownTimer
import android.text.TextUtils
import com.skoumal.teanity.databinding.ComparableRvItem
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
import com.skoumal.teanity.util.DiffObservableList
import com.skoumal.teanity.util.KObservableField
import com.topjohnwu.magisk.BuildConfig
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.magisk.data.database.MagiskDB
import com.topjohnwu.magisk.data.repository.AppRepository
import com.topjohnwu.magisk.model.entity.MagiskPolicy
import com.topjohnwu.magisk.model.entity.Policy
import com.topjohnwu.magisk.model.entity.recycler.SpinnerRvItem
import com.topjohnwu.magisk.model.entity.toPolicy
import com.topjohnwu.magisk.model.events.DieEvent
import com.topjohnwu.magisk.ui.base.MagiskViewModel
import com.topjohnwu.magisk.utils.FingerprintHelper
import com.topjohnwu.magisk.utils.SuConnector
import com.topjohnwu.magisk.utils.now
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
import me.tatarka.bindingcollectionadapter2.ItemBinding
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.TimeUnit.*
class SuRequestViewModel(
private val packageManager: PackageManager,
private val database: MagiskDB,
private val appRepo: AppRepository,
private val timeoutPrefs: SharedPreferences,
private val resources: Resources
) : MagiskViewModel() {
@@ -44,15 +49,20 @@ class SuRequestViewModel(
val canUseFingerprint = KObservableField(FingerprintHelper.useFingerprint())
val selectedItemPosition = KObservableField(0)
val items = DiffObservableList(ComparableRvItem.callback)
val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
private val items = DiffObservableList(ComparableRvItem.callback)
private val itemBinding = ItemBinding.of<ComparableRvItem<*>> { binding, _, item ->
item.bind(binding)
}
val adapter = BindingListViewAdapter<ComparableRvItem<*>>(1).apply {
itemBinding = this@SuRequestViewModel.itemBinding
setItems(items)
}
var handler: ActionHandler? = null
private var timer: CountDownTimer? = null
private var policy: Policy? = null
private var policy: MagiskPolicy? = null
set(value) {
field = value
updatePolicy(value)
@@ -62,12 +72,16 @@ class SuRequestViewModel(
resources.getStringArray(R.array.allow_timeout)
.map { SpinnerRvItem(it) }
.let { items.update(it) }
selectedItemPosition.addOnPropertyChangedCallback {
Timber.e("Changed position to $it")
}
}
private fun updatePolicy(policy: Policy?) {
private fun updatePolicy(policy: MagiskPolicy?) {
policy ?: return
icon.value = policy.info.loadIcon(packageManager)
icon.value = policy.applicationInfo.loadIcon(packageManager)
title.value = policy.appName
packageName.value = policy.packageName
@@ -94,7 +108,7 @@ class SuRequestViewModel(
return false
}
fun handleRequest(intent: Intent, createUICallback: () -> Unit): Boolean {
fun handleRequest(intent: Intent): Boolean {
val socketName = intent.getStringExtra("socket") ?: return false
val connector: SuConnector
@@ -107,8 +121,9 @@ class SuRequestViewModel(
}
val bundle = connector.readSocketInput()
val uid = bundle.getString("uid")?.toIntOrNull() ?: return false
database.clearOutdated()
policy = database.getPolicy(uid) ?: Policy(uid, packageManager)
appRepo.deleteOutdated().blockingGet() // wrong!
policy = runCatching { appRepo.fetch(uid).blockingGet() }
.getOrDefault(uid.toPolicy(packageManager))
} catch (e: IOException) {
e.printStackTrace()
return false
@@ -123,25 +138,26 @@ class SuRequestViewModel(
done()
}
@SuppressLint("ApplySharedPref")
override fun handleAction(action: Int) {
val pos = selectedItemPosition.value
timeoutPrefs.edit().putInt(policy?.packageName, pos).apply()
timeoutPrefs.edit().putInt(policy?.packageName, pos).commit()
handleAction(action, Config.Value.TIMEOUT_LIST[pos])
}
override fun handleAction(action: Int, time: Int) {
policy?.apply {
policy = action
if (time >= 0) {
until = if (time == 0) {
0
} else {
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
}
database.updatePolicy(this)
val until = if (time >= 0) {
if (time == 0) {
0
} else {
MILLISECONDS.toSeconds(now) + MINUTES.toSeconds(time.toLong())
}
} else {
policy?.until ?: 0
}
policy = policy?.copy(policy = action, until = until)?.apply {
appRepo.update(this).blockingGet()
}
policy?.policy = action
handleAction()
}
@@ -157,7 +173,7 @@ class SuRequestViewModel(
return true
}
when (Config.get<Any>(Config.Key.SU_AUTO_RESPONSE) as Int) {
when (Config.suAutoReponse) {
Config.Value.SU_AUTO_DENY -> {
handler?.handleAction(Policy.DENY, 0)
return true
@@ -168,15 +184,13 @@ class SuRequestViewModel(
}
}
createUICallback()
showUI()
return true
}
@SuppressLint("ClickableViewAccessibility")
private fun showUI() {
val seconds = Config.get<Int>(Config.Key.SU_REQUEST_TIMEOUT).toLong()
val millis = SECONDS.toMillis(seconds)
val millis = SECONDS.toMillis(Config.suDefaultTimeout.toLong())
timer = object : CountDownTimer(millis, 1000) {
override fun onTick(remains: Long) {
denyText.value = "%s (%d)"

View File

@@ -1,8 +1,6 @@
package com.topjohnwu.magisk.utils
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
@@ -13,8 +11,10 @@ import androidx.databinding.InverseBindingAdapter
import androidx.databinding.InverseBindingListener
import androidx.drawerlayout.widget.DrawerLayout
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.google.android.material.navigation.NavigationView
import com.skoumal.teanity.extensions.subscribeK
import com.topjohnwu.magisk.R
@@ -115,7 +115,7 @@ fun setMovieBehavior(view: TextView, isMovieBehavior: Boolean, text: String) {
}
}
@BindingAdapter("android:selectedItemPosition")
/*@BindingAdapter("selection"*//*, "selectionAttrChanged", "adapter"*//*)
fun setSelectedItemPosition(view: Spinner, position: Int) {
view.setSelection(position)
}
@@ -126,7 +126,7 @@ fun setSelectedItemPosition(view: Spinner, position: Int) {
)
fun getSelectedItemPosition(view: Spinner) = view.selectedItemPosition
@BindingAdapter("android:selectedItemPositionAttrChanged")
@BindingAdapter("selectedItemPositionAttrChanged")
fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListener) {
view.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(p0: AdapterView<*>?) {
@@ -137,7 +137,7 @@ fun setSelectedItemPositionListener(view: Spinner, listener: InverseBindingListe
listener.onChange()
}
}
}
}*/
@BindingAdapter("onTouch")
fun setOnTouchListener(view: View, listener: View.OnTouchListener) {
@@ -155,8 +155,6 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
Observable.timer(1, TimeUnit.SECONDS).subscribeK { callback() }
}
val tag = RecyclerView::class.java.name.sumBy { it.toInt() }
fun RecyclerView.Adapter<*>.setListener() {
val observer = object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
@@ -164,11 +162,12 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
}
}
registerAdapterDataObserver(observer)
view.setTag(tag, observer)
view.setTag(R.id.recyclerScrollListener, observer)
}
fun RecyclerView.Adapter<*>.removeListener() {
val observer = view.getTag(tag) as? RecyclerView.AdapterDataObserver ?: return
val observer =
view.getTag(R.id.recyclerScrollListener) as? RecyclerView.AdapterDataObserver ?: return
unregisterAdapterDataObserver(observer)
}
@@ -179,4 +178,40 @@ fun setScrollToLast(view: RecyclerView, shouldScrollToLast: Boolean) {
} else {
view.adapter?.removeListener()
}
}
}
@BindingAdapter("hide")
fun setHidden(view: FloatingActionButton, hide: Boolean) {
if (hide) view.hide() else view.show()
}
@BindingAdapter("scrollPosition", "scrollPositionSmooth", requireAll = false)
fun setScrollPosition(view: RecyclerView, position: Int, smoothScroll: Boolean) {
val adapterItemCount = view.adapter?.itemCount ?: -1
if (position !in 0 until adapterItemCount) {
// the position is not in adapter bounds, adapter will throw exception for invalid positions
return
}
when {
smoothScroll -> view.smoothScrollToPosition(position)
else -> view.scrollToPosition(position)
}
}
@BindingAdapter("recyclerScrollEvent")
fun setScrollListener(view: RecyclerView, listener: InverseBindingListener) {
view.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
// don't change this or the recycler will stop at every line, effectively disabling smooth scroll
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
listener.onChange()
}
}
})
}
@InverseBindingAdapter(attribute = "scrollPosition", event = "recyclerScrollEvent")
fun getScrollPosition(view: RecyclerView) = (view.layoutManager as? LinearLayoutManager)
?.findLastCompletelyVisibleItemPosition()
?: -1

View File

@@ -6,6 +6,7 @@ import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.ClassMap;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Info;
import com.topjohnwu.magisk.R;
import com.topjohnwu.magisk.ui.SplashActivity;
import com.topjohnwu.magisk.view.ProgressNotification;
@@ -24,8 +25,8 @@ public class DownloadApp {
}
public static void restore() {
String name = Utils.fmt("MagiskManager v%s(%d)",
Config.remoteManagerVersionString, Config.remoteManagerVersionCode);
String name = Utils.INSTANCE.fmt("MagiskManager v%s(%d)",
Info.remote.getApp().getVersion(), Info.remote.getApp().getVersionCode());
dlInstall(name, new RestoreManager());
}
@@ -33,7 +34,7 @@ public class DownloadApp {
File apk = new File(App.self.getCacheDir(), "manager.apk");
ProgressNotification progress = new ProgressNotification(name);
listener.progress = progress;
Networking.get(Config.managerLink)
Networking.get(Info.remote.getApp().getLink())
.setExecutor(App.THREAD_POOL)
.setDownloadProgressListener(progress)
.setErrorHandler((conn, e) -> progress.dlFail())

View File

@@ -1,123 +0,0 @@
package com.topjohnwu.magisk.utils;
import androidx.annotation.IntDef;
import androidx.collection.ArraySet;
import com.topjohnwu.superuser.internal.UiThreadHandler;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Set;
public class Event {
public static final int MAGISK_HIDE_DONE = 0;
public static final int MODULE_LOAD_DONE = 1;
public static final int REPO_LOAD_DONE = 2;
public static final int UPDATE_CHECK_DONE = 3;
public static final int LOCALE_FETCH_DONE = 4;
@IntDef({MAGISK_HIDE_DONE, MODULE_LOAD_DONE, REPO_LOAD_DONE,
UPDATE_CHECK_DONE, LOCALE_FETCH_DONE})
@Retention(RetentionPolicy.SOURCE)
public @interface EventID {}
// We will not dynamically add topics, so use arrays instead of hash tables
private static Store[] eventList = new Store[5];
public static void register(Listener listener, @EventID int... events) {
for (int event : events) {
if (eventList[event] == null)
eventList[event] = new Store();
eventList[event].listeners.add(listener);
if (eventList[event].triggered) {
listener.onEvent(event);
}
}
}
public static void register(AutoListener listener) {
register(listener, listener.getListeningEvents());
}
public static void unregister(Listener listener, @EventID int... events) {
for (int event : events) {
if (eventList[event] == null)
continue;
eventList[event].listeners.remove(listener);
}
}
public static void unregister(AutoListener listener) {
unregister(listener, listener.getListeningEvents());
}
public static void trigger(@EventID int event) {
trigger(true, event, null);
}
public static void trigger(@EventID int event, Object result) {
trigger(true, event, result);
}
public static void trigger(boolean perm, @EventID int event) {
trigger(perm, event, null);
}
public static void trigger(boolean perm, @EventID int event, Object result) {
if (eventList[event] == null)
eventList[event] = new Store();
if (perm) {
eventList[event].result = result;
eventList[event].triggered = true;
}
for (Listener sub : eventList[event].listeners) {
UiThreadHandler.run(() -> sub.onEvent(event));
}
}
public static void reset(@EventID int event) {
if (eventList[event] == null)
return;
eventList[event].triggered = false;
eventList[event].result = null;
}
public static void reset(AutoListener listener) {
for (int event : listener.getListeningEvents())
reset(event);
}
public static boolean isTriggered(@EventID int event) {
if (eventList[event] == null)
return false;
return eventList[event].triggered;
}
public static boolean isTriggered(AutoListener listener) {
for (int event : listener.getListeningEvents()) {
if (!isTriggered(event))
return false;
}
return true;
}
public static <T> T getResult(@EventID int event) {
return (T) eventList[event].result;
}
private static class Store {
boolean triggered = false;
Set<Listener> listeners = new ArraySet<>();
Object result;
}
public interface Listener {
void onEvent(int event);
}
public interface AutoListener extends Listener {
@EventID
int[] getListeningEvents();
}
}

View File

@@ -1,123 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.magisk.Const;
import java.security.KeyStore;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
@TargetApi(Build.VERSION_CODES.M)
public abstract class FingerprintHelper {
private FingerprintManager manager;
private Cipher cipher;
private CancellationSignal cancel;
public static boolean useFingerprint() {
boolean fp = Config.get(Config.Key.SU_FINGERPRINT);
if (fp && !canUseFingerprint()) {
Config.set(Config.Key.SU_FINGERPRINT, false);
fp = false;
}
return fp;
}
public static boolean canUseFingerprint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return false;
KeyguardManager km = App.self.getSystemService(KeyguardManager.class);
FingerprintManager fm = App.self.getSystemService(FingerprintManager.class);
return km.isKeyguardSecure() && fm != null && fm.isHardwareDetected() && fm.hasEnrolledFingerprints();
}
protected FingerprintHelper() throws Exception {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
manager = App.self.getSystemService(FingerprintManager.class);
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey(Const.SU_KEYSTORE_KEY, null);
if (key == null) {
key = generateKey();
}
try {
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (KeyPermanentlyInvalidatedException e) {
// Only happens on Marshmallow
key = generateKey();
cipher.init(Cipher.ENCRYPT_MODE, key);
}
}
public abstract void onAuthenticationError(int errorCode, CharSequence errString);
public abstract void onAuthenticationHelp(int helpCode, CharSequence helpString);
public abstract void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result);
public abstract void onAuthenticationFailed();
public void authenticate() {
cancel = new CancellationSignal();
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
manager.authenticate(cryptoObject, cancel, 0, new Callback(), null);
}
public void cancel() {
if (cancel != null)
cancel.cancel();
}
private SecretKey generateKey() throws Exception {
KeyGenerator keygen = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
Const.SU_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(false);
}
keygen.init(builder.build());
return keygen.generateKey();
}
private class Callback extends FingerprintManager.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, CharSequence errString) {
FingerprintHelper.this.onAuthenticationError(errorCode, errString);
}
@Override
public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
FingerprintHelper.this.onAuthenticationHelp(helpCode, helpString);
}
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
FingerprintHelper.this.onAuthenticationSucceeded(result);
}
@Override
public void onAuthenticationFailed() {
FingerprintHelper.this.onAuthenticationFailed();
}
}
}

View File

@@ -0,0 +1,119 @@
package com.topjohnwu.magisk.utils
import android.annotation.TargetApi
import android.app.KeyguardManager
import android.content.Context
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import com.topjohnwu.magisk.Config
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
@TargetApi(Build.VERSION_CODES.M)
abstract class FingerprintHelper @Throws(Exception::class)
protected constructor() {
private val manager: FingerprintManager?
private val cipher: Cipher
private var cancel: CancellationSignal? = null
private val context: Context by inject()
init {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
manager = context.getSystemService(FingerprintManager::class.java)
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyStore.load(null)
var key = keyStore.getKey(SU_KEYSTORE_KEY, null) as SecretKey? ?: generateKey()
runCatching {
cipher.init(Cipher.ENCRYPT_MODE, key)
}.onFailure {
// Only happens on Marshmallow
key = generateKey()
cipher.init(Cipher.ENCRYPT_MODE, key)
}
}
abstract fun onAuthenticationError(errorCode: Int, errString: CharSequence)
abstract fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence)
abstract fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult)
abstract fun onAuthenticationFailed()
fun authenticate() {
cancel = CancellationSignal()
val cryptoObject = FingerprintManager.CryptoObject(cipher)
manager!!.authenticate(cryptoObject, cancel, 0, Callback(), null)
}
fun cancel() {
if (cancel != null)
cancel!!.cancel()
}
@Throws(Exception::class)
private fun generateKey(): SecretKey {
val keygen = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(
SU_KEYSTORE_KEY,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setInvalidatedByBiometricEnrollment(false)
}
keygen.init(builder.build())
return keygen.generateKey()
}
private inner class Callback : FingerprintManager.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
this@FingerprintHelper.onAuthenticationError(errorCode, errString)
}
override fun onAuthenticationHelp(helpCode: Int, helpString: CharSequence) {
this@FingerprintHelper.onAuthenticationHelp(helpCode, helpString)
}
override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
this@FingerprintHelper.onAuthenticationSucceeded(result)
}
override fun onAuthenticationFailed() {
this@FingerprintHelper.onAuthenticationFailed()
}
}
companion object {
private const val SU_KEYSTORE_KEY = "su_key"
fun useFingerprint(): Boolean {
var fp = Config.suFingerprint
if (fp && !canUseFingerprint()) {
Config.suFingerprint = false
fp = false
}
return fp
}
fun canUseFingerprint(context: Context = get()): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return false
val km = context.getSystemService(KeyguardManager::class.java)
val fm = context.getSystemService(FingerprintManager::class.java)
return km?.isKeyguardSecure ?: false &&
fm != null && fm.isHardwareDetected && fm.hasEnrolledFingerprints()
}
}
}

View File

@@ -1,147 +0,0 @@
package com.topjohnwu.magisk.utils;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.StringRes;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.Config;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.internal.InternalUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
public class LocaleManager {
public static Locale locale = Locale.getDefault();
public final static Locale defaultLocale = Locale.getDefault();
public static List<Locale> locales;
public static Locale forLanguageTag(String tag) {
if (Build.VERSION.SDK_INT >= 21) {
return Locale.forLanguageTag(tag);
} else {
String[] tok = tag.split("-");
if (tok.length == 0) {
return new Locale("");
}
String language;
switch (tok[0]) {
case "und":
language = ""; // Undefined
break;
case "fil":
language = "tl"; // Filipino
break;
default:
language = tok[0];
}
if ((language.length() != 2 && language.length() != 3))
return new Locale("");
if (tok.length == 1)
return new Locale(language);
String country = tok[1];
if (country.length() != 2 && country.length() != 3)
return new Locale(language);
return new Locale(language, country);
}
}
public static String toLanguageTag(Locale loc) {
if (Build.VERSION.SDK_INT >= 21) {
return loc.toLanguageTag();
} else {
String language = loc.getLanguage();
String country = loc.getCountry();
String variant = loc.getVariant();
if (language.isEmpty() || !language.matches("\\p{Alpha}{2,8}")) {
language = "und"; // Follow the Locale#toLanguageTag() implementation
} else if (language.equals("iw")) {
language = "he"; // correct deprecated "Hebrew"
} else if (language.equals("in")) {
language = "id"; // correct deprecated "Indonesian"
} else if (language.equals("ji")) {
language = "yi"; // correct deprecated "Yiddish"
}
// ensure valid country code, if not well formed, it's omitted
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}")) {
country = "";
}
// variant subtags that begin with a letter must be at least 5 characters long
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}")) {
variant = "";
}
StringBuilder tag = new StringBuilder(language);
if (!country.isEmpty())
tag.append('-').append(country);
if (!variant.isEmpty())
tag.append('-').append(variant);
return tag.toString();
}
}
public static void setLocale(ContextWrapper wrapper) {
String localeConfig = Config.get(Config.Key.LOCALE);
if (TextUtils.isEmpty(localeConfig)) {
locale = defaultLocale;
} else {
locale = forLanguageTag(localeConfig);
}
Locale.setDefault(locale);
InternalUtils.replaceBaseContext(wrapper, getLocaleContext(locale));
}
public static Context getLocaleContext(Context context, Locale locale) {
Configuration config = new Configuration(context.getResources().getConfiguration());
config.setLocale(locale);
return context.createConfigurationContext(config);
}
public static Context getLocaleContext(Locale locale) {
return getLocaleContext(App.self.getBaseContext(), locale);
}
public static String getString(Locale locale, @StringRes int id) {
return getLocaleContext(locale).getString(id);
}
public static void loadAvailableLocales(@StringRes int compareId) {
Shell.EXECUTOR.execute(() -> {
locales = new ArrayList<>();
HashSet<String> set = new HashSet<>();
Resources res = App.self.getResources();
Locale locale;
// Add default locale
locales.add(Locale.ENGLISH);
set.add(getString(Locale.ENGLISH, compareId));
// Add some special locales
locales.add(Locale.TAIWAN);
set.add(getString(Locale.TAIWAN, compareId));
locale = new Locale("pt", "BR");
locales.add(locale);
set.add(getString(locale, compareId));
// Other locales
for (String s : res.getAssets().getLocales()) {
locale = forLanguageTag(s);
if (set.add(getString(locale, compareId))) {
locales.add(locale);
}
}
Collections.sort(locales, (a, b) -> a.getDisplayName(a).compareTo(b.getDisplayName(b)));
Event.trigger(Event.LOCALE_FETCH_DONE);
});
}
}

View File

@@ -0,0 +1,135 @@
package com.topjohnwu.magisk.utils
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import androidx.annotation.StringRes
import com.topjohnwu.magisk.App
import com.topjohnwu.magisk.Config
import com.topjohnwu.magisk.R
import com.topjohnwu.superuser.internal.InternalUtils
import io.reactivex.Single
import java.util.*
object LocaleManager {
@JvmStatic
var locale = Locale.getDefault()
@JvmStatic
val defaultLocale = Locale.getDefault()
@JvmStatic
val availableLocales = Single.fromCallable {
val compareId = R.string.app_changelog
val res: Resources by inject()
mutableListOf<Locale>().apply {
// Add default locale
add(Locale.ENGLISH)
// Add some special locales
add(Locale.TAIWAN)
add(Locale("pt", "BR"))
// Other locales
val otherLocales = res.assets.locales
.map { forLanguageTag(it) }
.distinctBy { getString(it, compareId) }
listOf("", "").toTypedArray()
addAll(otherLocales)
}.sortedWith(Comparator { a, b ->
a.getDisplayName(a).toLowerCase(a)
.compareTo(b.getDisplayName(b).toLowerCase(b))
})
}.cache()
private fun forLanguageTag(tag: String): Locale {
if (Build.VERSION.SDK_INT >= 21) {
return Locale.forLanguageTag(tag)
} else {
val tok = tag.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (tok.isEmpty()) {
return Locale("")
}
val language = when (tok[0]) {
"und" -> "" // Undefined
"fil" -> "tl" // Filipino
else -> tok[0]
}
if (language.length != 2 && language.length != 3)
return Locale("")
if (tok.size == 1)
return Locale(language)
val country = tok[1]
return if (country.length != 2 && country.length != 3) Locale(language)
else Locale(language, country)
}
}
@JvmStatic
fun toLanguageTag(loc: Locale): String {
if (Build.VERSION.SDK_INT >= 21) {
return loc.toLanguageTag()
} else {
var language = loc.language
var country = loc.country
var variant = loc.variant
when {
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
language = "und" // Follow the Locale#toLanguageTag() implementation
language == "iw" -> language = "he" // correct deprecated "Hebrew"
language == "in" -> language = "id" // correct deprecated "Indonesian"
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
}
// ensure valid country code, if not well formed, it's omitted
// variant subtags that begin with a letter must be at least 5 characters long
// ensure valid country code, if not well formed, it's omitted
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
country = ""
}
// variant subtags that begin with a letter must be at least 5 characters long
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
variant = ""
}
val tag = StringBuilder(language)
if (country.isNotEmpty())
tag.append('-').append(country)
if (variant.isNotEmpty())
tag.append('-').append(variant)
return tag.toString()
}
}
@JvmStatic
fun setLocale(wrapper: ContextWrapper) {
val localeConfig = Config.locale
locale = when {
localeConfig.isEmpty() -> defaultLocale
else -> forLanguageTag(localeConfig)
}
Locale.setDefault(locale)
InternalUtils.replaceBaseContext(wrapper, getLocaleContext(locale))
}
@JvmStatic
fun getLocaleContext(context: Context, locale: Locale): Context {
val config = Configuration(context.resources.configuration)
config.setLocale(locale)
return context.createConfigurationContext(config)
}
@JvmStatic
fun getLocaleContext(locale: Locale): Context {
return getLocaleContext(App.self.baseContext, locale)
}
@JvmStatic
fun getString(locale: Locale, @StringRes id: Int): String {
return getLocaleContext(locale).getString(id)
}
}

View File

@@ -13,7 +13,7 @@ public class Logger {
}
public static void debug(String fmt, Object... args) {
debug(Utils.fmt(fmt, args));
debug(Utils.INSTANCE.fmt(fmt, args));
}
public static void error(String line) {
@@ -21,6 +21,6 @@ public class Logger {
}
public static void error(String fmt, Object... args) {
error(Utils.fmt(fmt, args));
error(Utils.INSTANCE.fmt(fmt, args));
}
}

View File

@@ -3,6 +3,8 @@ package com.topjohnwu.magisk.utils;
import android.content.ComponentName;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import com.topjohnwu.magisk.App;
import com.topjohnwu.magisk.BuildConfig;
import com.topjohnwu.magisk.ClassMap;
@@ -27,8 +29,6 @@ import java.util.ArrayList;
import java.util.List;
import java.util.jar.JarEntry;
import androidx.core.app.NotificationCompat;
public class PatchAPK {
public static final String LOWERALPHA = "abcdefghijklmnopqrstuvwxyz";
@@ -110,7 +110,7 @@ public class PatchAPK {
if (!Shell.su("pm install " + repack).exec().isSuccess())
return false;
Config.set(Config.Key.SU_MANAGER, pkg);
Config.setSuManager(pkg);
Config.export();
RootUtils.rmAndLaunch(BuildConfig.APPLICATION_ID,
new ComponentName(pkg, ClassMap.get(SplashActivity.class).getName()));
@@ -145,7 +145,7 @@ public class PatchAPK {
Notifications.progress(app.getString(R.string.hide_manager_title));
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build());
if(!patchAndHide())
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
Utils.INSTANCE.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG);
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID);
});
}

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