mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-20 03:27:36 +00:00
Compare commits
257 Commits
v19.3
...
manager-v7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
19906575a3 | ||
![]() |
9329094a4e | ||
![]() |
b44f5122fd | ||
![]() |
17981730a4 | ||
![]() |
53de6da26c | ||
![]() |
3e30ccdeee | ||
![]() |
baaaf7d5de | ||
![]() |
45d8d139a9 | ||
![]() |
fe644e10d0 | ||
![]() |
f383d11d10 | ||
![]() |
ef1b928532 | ||
![]() |
6e46d394b1 | ||
![]() |
f109038d12 | ||
![]() |
e31e687602 | ||
![]() |
86bfb22d4c | ||
![]() |
3f057367e3 | ||
![]() |
3d7ed5820e | ||
![]() |
0118f2efa7 | ||
![]() |
15312e4709 | ||
![]() |
bf1568a73a | ||
![]() |
13a2520ea5 | ||
![]() |
f53238f206 | ||
![]() |
9375748d9b | ||
![]() |
201df54e79 | ||
![]() |
0b54fe477b | ||
![]() |
4119e6669e | ||
![]() |
d33e5226b3 | ||
![]() |
d73f39c706 | ||
![]() |
087b451e17 | ||
![]() |
86481c74ff | ||
![]() |
5b937fb1fa | ||
![]() |
ff828116bc | ||
![]() |
ee39616a8b | ||
![]() |
cdb53ca049 | ||
![]() |
8cf475f708 | ||
![]() |
0cb449e1d6 | ||
![]() |
e6adb7abca | ||
![]() |
cfad7dd317 | ||
![]() |
dd35224f92 | ||
![]() |
1283590eeb | ||
![]() |
dca3fe396f | ||
![]() |
8d87eae11b | ||
![]() |
fd7eaacae0 | ||
![]() |
fba33cbbe9 | ||
![]() |
950ffcd790 | ||
![]() |
c178299013 | ||
![]() |
5d17c1f588 | ||
![]() |
a75c00d94e | ||
![]() |
cd19517414 | ||
![]() |
155f39aab5 | ||
![]() |
4514d0b467 | ||
![]() |
6f4a938a31 | ||
![]() |
1303ea95dd | ||
![]() |
727fe1bd15 | ||
![]() |
64ebc977e9 | ||
![]() |
e89c50d934 | ||
![]() |
c859ddfb8f | ||
![]() |
a6126c5eda | ||
![]() |
85d9bd9106 | ||
![]() |
39e9622205 | ||
![]() |
021994c9f3 | ||
![]() |
2e7ce2a769 | ||
![]() |
84f0ff2fad | ||
![]() |
e6561e5f84 | ||
![]() |
5fa452aa74 | ||
![]() |
2225ccb146 | ||
![]() |
5aafc78847 | ||
![]() |
0d03833cff | ||
![]() |
a797d5d396 | ||
![]() |
f2494374f8 | ||
![]() |
48395ba860 | ||
![]() |
5ba5f5f94e | ||
![]() |
42ce6fd334 | ||
![]() |
f5c3ee3ae1 | ||
![]() |
3c7ece1605 | ||
![]() |
870efc49ea | ||
![]() |
085ede6d93 | ||
![]() |
4ef19d17da | ||
![]() |
223913c30a | ||
![]() |
010e4de4e1 | ||
![]() |
41134466ed | ||
![]() |
8f07747452 | ||
![]() |
eb5ce5be1e | ||
![]() |
71d855e836 | ||
![]() |
33b7ab593c | ||
![]() |
8706d834b4 | ||
![]() |
7cfab33ebb | ||
![]() |
1ababc8c7f | ||
![]() |
1f75e63c37 | ||
![]() |
cb3f9b9740 | ||
![]() |
9784353223 | ||
![]() |
7d93ca5c73 | ||
![]() |
ac20063e86 | ||
![]() |
debaec32af | ||
![]() |
0e9b71e7a9 | ||
![]() |
85f5ff3c14 | ||
![]() |
3d81f167ea | ||
![]() |
fb70a2e52d | ||
![]() |
460e85a1b5 | ||
![]() |
539b64bd57 | ||
![]() |
90e38a06a2 | ||
![]() |
09ab910630 | ||
![]() |
c15f80b33f | ||
![]() |
b2e6ba3c4a | ||
![]() |
b16f696b0e | ||
![]() |
9adfb382e8 | ||
![]() |
44368383f4 | ||
![]() |
d1ff7e0ffe | ||
![]() |
42e7db8d13 | ||
![]() |
0c17ea5755 | ||
![]() |
cdaff5b39c | ||
![]() |
2b1b970e78 | ||
![]() |
0aebc0a8e3 | ||
![]() |
c3a89f589e | ||
![]() |
971cd73fb3 | ||
![]() |
1947860d61 | ||
![]() |
55aaa421e8 | ||
![]() |
a8932706d8 | ||
![]() |
a97972aac0 | ||
![]() |
094c3d559a | ||
![]() |
6fb032b3c2 | ||
![]() |
8ca188f4d4 | ||
![]() |
746a1d8d59 | ||
![]() |
63c5e00d86 | ||
![]() |
9d2e5d6665 | ||
![]() |
f6045bf8b5 | ||
![]() |
e83f40d5c5 | ||
![]() |
e5118418b2 | ||
![]() |
7cd814d917 | ||
![]() |
78282c1a49 | ||
![]() |
fd4214ccf3 | ||
![]() |
0785945635 | ||
![]() |
967bdeae7b | ||
![]() |
452db51669 | ||
![]() |
5875ced367 | ||
![]() |
fbac6bcfd0 | ||
![]() |
0dcd3ece9d | ||
![]() |
224fff89e3 | ||
![]() |
22e73644f9 | ||
![]() |
6a0f6ab319 | ||
![]() |
88a394836f | ||
![]() |
f822c1c2e4 | ||
![]() |
1d16d980b3 | ||
![]() |
501b18f986 | ||
![]() |
21ed759e53 | ||
![]() |
8d50dfd93c | ||
![]() |
51e40dd98c | ||
![]() |
b2048379af | ||
![]() |
011539f6f1 | ||
![]() |
5457c3803f | ||
![]() |
b3d777bb6c | ||
![]() |
12e00c3054 | ||
![]() |
40b683111c | ||
![]() |
9542ca773f | ||
![]() |
8af832a496 | ||
![]() |
6836130fda | ||
![]() |
724893879f | ||
![]() |
736729f5ef | ||
![]() |
aa47966347 | ||
![]() |
d64d12afe8 | ||
![]() |
1f8df419c4 | ||
![]() |
7ba8202af5 | ||
![]() |
d7b691cf59 | ||
![]() |
7058d5e4cd | ||
![]() |
52fd508fea | ||
![]() |
41045b62dc | ||
![]() |
188ea2644a | ||
![]() |
4c8f357978 | ||
![]() |
4bb2fd6ba6 | ||
![]() |
33c9f74508 | ||
![]() |
f53fe67372 | ||
![]() |
51ff724691 | ||
![]() |
291bf93f9d | ||
![]() |
5fcd629f16 | ||
![]() |
ab90901793 | ||
![]() |
4f206fd918 | ||
![]() |
7233285437 | ||
![]() |
8e348a11c2 | ||
![]() |
085ea6d0a1 | ||
![]() |
aaf88b1895 | ||
![]() |
4f4a9412a3 | ||
![]() |
a92e039363 | ||
![]() |
33aa4ca4b7 | ||
![]() |
05658cafc7 | ||
![]() |
ff3710de66 | ||
![]() |
db8dd9f186 | ||
![]() |
e8b73ba6d1 | ||
![]() |
f1112fdf37 | ||
![]() |
a48c4f9e05 | ||
![]() |
19a521d2e9 | ||
![]() |
dd6e55ac31 | ||
![]() |
b1e63f0f14 | ||
![]() |
b0e49a4cc8 | ||
![]() |
1e94517a72 | ||
![]() |
98f60216ac | ||
![]() |
e29b712108 | ||
![]() |
a462435f2f | ||
![]() |
911b8273fe | ||
![]() |
09935e591a | ||
![]() |
4a212dba35 | ||
![]() |
aac9e85e04 | ||
![]() |
bb67a837d3 | ||
![]() |
6cde695194 | ||
![]() |
a1a1ac0bbb | ||
![]() |
9ec8bc2166 | ||
![]() |
28cd6a75e7 | ||
![]() |
4cc7aced15 | ||
![]() |
1058aeb04f | ||
![]() |
cfec0db947 | ||
![]() |
120bd6cd68 | ||
![]() |
9aef06d1b8 | ||
![]() |
e6e9dd751c | ||
![]() |
5dd677756f | ||
![]() |
b77c590910 | ||
![]() |
7e5f2822ae | ||
![]() |
12bbc7fd6b | ||
![]() |
bf9ac8252b | ||
![]() |
4a3f5dc619 | ||
![]() |
ca156befbd | ||
![]() |
4db41e2ac4 | ||
![]() |
982a43fce1 | ||
![]() |
dd76a74e1c | ||
![]() |
70cb52b2c7 | ||
![]() |
5c7f69acaa | ||
![]() |
f1d9015e5f | ||
![]() |
e8d900c58e | ||
![]() |
a6241ae912 | ||
![]() |
4a697ca2ec | ||
![]() |
58bec7f2c9 | ||
![]() |
213f84985c | ||
![]() |
074b1f8c61 | ||
![]() |
326eee8c83 | ||
![]() |
00bff4912e | ||
![]() |
0ce1720516 | ||
![]() |
ee407472cf | ||
![]() |
f341f3b2dd | ||
![]() |
8513946e09 | ||
![]() |
8ebd9c8927 | ||
![]() |
1d54c5144e | ||
![]() |
e40d4318fa | ||
![]() |
7756e10779 | ||
![]() |
3e58d502d0 | ||
![]() |
1c8846dc57 | ||
![]() |
2f320c7239 | ||
![]() |
e799918ab6 | ||
![]() |
86c4928e0f | ||
![]() |
0293eb5c51 | ||
![]() |
1ee75b6aa6 | ||
![]() |
4b30b224b5 | ||
![]() |
16b232d2a3 | ||
![]() |
3f3b1f5b1d | ||
![]() |
cec017b7bf | ||
![]() |
3123cc1059 | ||
![]() |
caa9df86bc | ||
![]() |
f417389a7a | ||
![]() |
662a5c8ea6 | ||
![]() |
7edfbfb764 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -11,7 +11,7 @@
|
||||
*.bat text eol=crlf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
chromeos/** binary
|
||||
tools/** binary
|
||||
*.jar binary
|
||||
*.exe binary
|
||||
*.apk binary
|
||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -19,3 +19,9 @@
|
||||
[submodule "nanopb"]
|
||||
path = native/jni/external/nanopb
|
||||
url = https://github.com/nanopb/nanopb.git
|
||||
[submodule "mincrypt"]
|
||||
path = native/jni/external/mincrypt
|
||||
url = https://github.com/topjohnwu/mincrypt.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
@@ -10,7 +10,7 @@ Furthermore, Magisk provides a **Systemless Interface** to alter the system (or
|
||||
|
||||
## Bug Reports
|
||||
|
||||
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that is already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by opening an issue on GitHub or directly in the thread.
|
||||
**Make sure to install the latest [Canary Build](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337) before reporting any bugs!** **DO NOT** report bugs that are already fixed upstream. Follow the instructions in the [Canary Channel XDA Thread](https://forum.xda-developers.com/apps/magisk/dev-magisk-canary-channel-bleeding-edge-t3839337), and report a bug either by [opening an issue on GitHub](https://github.com/topjohnwu/Magisk/issues) or directly in the thread.
|
||||
|
||||
## Building Environment Requirements
|
||||
|
||||
|
@@ -17,8 +17,8 @@ android {
|
||||
applicationId 'com.topjohnwu.magisk'
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled true
|
||||
versionName configProps['appVersion']
|
||||
versionCode configProps['appVersionCode'] as Integer
|
||||
versionName props['appVersion']
|
||||
versionCode props['appVersionCode'] as Integer
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -39,10 +39,14 @@ android {
|
||||
exclude '/META-INF/*.kotlin_module'
|
||||
exclude '/META-INF/rxkotlin.properties'
|
||||
exclude '/androidsupportmultidexversion.txt'
|
||||
exclude '/org/**'
|
||||
exclude '/org/bouncycastle/**'
|
||||
exclude '/kotlin/**'
|
||||
exclude '/kotlinx/**'
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = '1.8'
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
@@ -51,7 +55,6 @@ androidExtensions {
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation project(':net')
|
||||
implementation project(':shared')
|
||||
implementation project(':signing')
|
||||
|
||||
@@ -59,13 +62,14 @@ dependencies {
|
||||
implementation 'com.jakewharton.timber:timber:4.7.1'
|
||||
implementation 'com.github.skoumalcz:teanity:0.3.3'
|
||||
implementation 'com.ncapdevi:frag-nav:3.2.0'
|
||||
implementation 'com.github.pwittchen:reactivenetwork-rx2:3.0.6'
|
||||
|
||||
def vMarkwon = '3.0.1'
|
||||
def vMarkwon = '3.1.0'
|
||||
implementation "ru.noties.markwon:core:${vMarkwon}"
|
||||
implementation "ru.noties.markwon:html:${vMarkwon}"
|
||||
implementation "ru.noties.markwon:image-svg:${vMarkwon}"
|
||||
|
||||
def vLibsu = '2.5.0'
|
||||
def vLibsu = '2.5.1'
|
||||
implementation "com.github.topjohnwu.libsu:core:${vLibsu}"
|
||||
implementation "com.github.topjohnwu.libsu:io:${vLibsu}"
|
||||
|
||||
@@ -74,12 +78,13 @@ dependencies {
|
||||
implementation "org.koin:koin-android:${vKoin}"
|
||||
implementation "org.koin:koin-androidx-viewmodel:${vKoin}"
|
||||
|
||||
def vRetrofit = "2.5.0"
|
||||
def vRetrofit = '2.6.1'
|
||||
implementation "com.squareup.retrofit2:retrofit:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-moshi:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:converter-scalars:${vRetrofit}"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:${vRetrofit}"
|
||||
|
||||
def vOkHttp = "3.12.0"
|
||||
def vOkHttp = '3.12.2'
|
||||
implementation "com.squareup.okhttp3:okhttp:${vOkHttp}"
|
||||
implementation "com.squareup.okhttp3:logging-interceptor:${vOkHttp}"
|
||||
|
||||
@@ -90,13 +95,25 @@ dependencies {
|
||||
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}"
|
||||
kapt "androidx.room:room-compiler:${vRoom}"
|
||||
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVer}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlinVer}"
|
||||
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-alpha05'
|
||||
implementation 'androidx.preference:preference:1.1.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0-beta04'
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.work:work-runtime:2.0.1'
|
||||
implementation 'androidx.transition:transition:1.2.0-alpha01'
|
||||
implementation 'androidx.work:work-runtime:2.2.0'
|
||||
implementation 'androidx.transition:transition:1.2.0-rc01'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha06'
|
||||
implementation 'com.google.android.material:material:1.1.0-alpha10'
|
||||
}
|
||||
|
12
app/proguard-rules.pro
vendored
12
app/proguard-rules.pro
vendored
@@ -16,13 +16,10 @@
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Retrofit classes
|
||||
-keep,allowobfuscation class com.topjohnwu.magisk.data.network.*
|
||||
|
||||
# Snet
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.ISafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.ISafetyNetHelper$Callback {
|
||||
-keepclassmembers class com.topjohnwu.magisk.utils.SafetyNetHelper { *; }
|
||||
-keep,allowobfuscation interface com.topjohnwu.magisk.utils.SafetyNetHelper$Callback
|
||||
-keepclassmembers class * implements com.topjohnwu.magisk.utils.SafetyNetHelper$Callback {
|
||||
void onResponse(int);
|
||||
}
|
||||
|
||||
@@ -39,9 +36,6 @@
|
||||
|
||||
# Strip logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree { *; }
|
||||
-assumenosideeffects class com.topjohnwu.magisk.utils.Logger {
|
||||
public *** debug(...);
|
||||
}
|
||||
|
||||
# Excessive obfuscation
|
||||
-repackageclasses 'a'
|
||||
|
@@ -3,14 +3,15 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.topjohnwu.magisk">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:name="a.e"
|
||||
android:allowBackup="true"
|
||||
android:theme="@style/MagiskTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
@@ -41,9 +42,9 @@
|
||||
|
||||
<activity
|
||||
android:name="a.m"
|
||||
android:exported="false"
|
||||
android:directBootAware="true"
|
||||
android:excludeFromRecents="true"
|
||||
android:exported="false"
|
||||
android:theme="@style/MagiskTheme.SU" />
|
||||
|
||||
<!-- Receiver -->
|
||||
@@ -65,7 +66,8 @@
|
||||
|
||||
<!-- Service -->
|
||||
|
||||
<service android:name="a.j" />
|
||||
<service android:name="a.j"
|
||||
android:exported="false" />
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package a;
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService;
|
||||
import com.topjohnwu.magisk.model.download.DownloadService;
|
||||
|
||||
public class j extends DownloadModuleService {
|
||||
public class j extends DownloadService {
|
||||
/* stub */
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package a;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.work.Worker;
|
||||
import androidx.work.WorkerParameters;
|
||||
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
|
||||
/* Wrapper class to workaround Proguard -keep class * extends Worker */
|
||||
@@ -22,7 +22,7 @@ public abstract class w<T extends DelegateWorker> extends Worker {
|
||||
try {
|
||||
base = ((Class<T>) ((ParameterizedType) getClass().getGenericSuperclass())
|
||||
.getActualTypeArguments()[0]).newInstance();
|
||||
base.setActualWorker(this);
|
||||
base.attachWorker(this);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
|
@@ -1,32 +1,42 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.AsyncTask
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.multidex.MultiDex
|
||||
import androidx.room.Room
|
||||
import androidx.work.impl.WorkDatabase
|
||||
import androidx.work.impl.WorkDatabase_Impl
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase
|
||||
import com.topjohnwu.magisk.data.database.RepoDatabase_Impl
|
||||
import com.topjohnwu.magisk.di.ActivityTracker
|
||||
import com.topjohnwu.magisk.di.koinModules
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.magisk.utils.LocaleManager
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.net.Networking
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ThreadPoolExecutor
|
||||
|
||||
open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
open class App : Application() {
|
||||
|
||||
lateinit var protectedContext: Context
|
||||
|
||||
@Volatile
|
||||
private var foreground: Activity? = null
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
Room.setFactory {
|
||||
when (it) {
|
||||
WorkDatabase::class.java -> WorkDatabase_Impl()
|
||||
RepoDatabase::class.java -> RepoDatabase_Impl()
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(base)
|
||||
@@ -39,17 +49,7 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
modules(koinModules)
|
||||
}
|
||||
|
||||
protectedContext = baseContext
|
||||
self = this
|
||||
deContext = base
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 24) {
|
||||
protectedContext = base.createDeviceProtectedStorageContext()
|
||||
deContext = protectedContext
|
||||
deContext.moveSharedPreferencesFrom(base, base.defaultPrefsName)
|
||||
}
|
||||
|
||||
registerActivityLifecycleCallbacks(this)
|
||||
registerActivityLifecycleCallbacks(get<ActivityTracker>())
|
||||
|
||||
Networking.init(base)
|
||||
LocaleManager.setLocale(this)
|
||||
@@ -59,61 +59,4 @@ open class App : Application(), Application.ActivityLifecycleCallbacks {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleManager.setLocale(this)
|
||||
}
|
||||
|
||||
//region ActivityLifecycleCallbacks
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
//endregion
|
||||
|
||||
private val Context.defaultPrefsName get() = "${packageName}_preferences"
|
||||
|
||||
companion object {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection")
|
||||
@JvmStatic
|
||||
lateinit var self: App
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Deprecated("Use dependency injection; replace with protectedContext")
|
||||
@JvmStatic
|
||||
lateinit var deContext: Context
|
||||
|
||||
@Deprecated("Use Rx or similar")
|
||||
@JvmField
|
||||
var THREAD_POOL: ThreadPoolExecutor
|
||||
|
||||
init {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
|
||||
Shell.Config.setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_USE_MAGISK_BUSYBOX)
|
||||
Shell.Config.verboseLogging(BuildConfig.DEBUG)
|
||||
Shell.Config.addInitializers(RootUtils::class.java)
|
||||
Shell.Config.setTimeout(2)
|
||||
THREAD_POOL = AsyncTask.THREAD_POOL_EXECUTOR as ThreadPoolExecutor
|
||||
}
|
||||
|
||||
@Deprecated("")
|
||||
@JvmStatic
|
||||
fun foreground(): Activity? {
|
||||
val app: App by inject()
|
||||
return app.foreground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.download.DownloadModuleService
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.receiver.GeneralReceiver
|
||||
import com.topjohnwu.magisk.model.update.UpdateCheckService
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
@@ -16,12 +16,11 @@ object ClassMap {
|
||||
FlashActivity::class.java to a.f::class.java,
|
||||
UpdateCheckService::class.java to a.g::class.java,
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
DownloadModuleService::class.java to a.j::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
operator fun get(c: Class<*>): Class<*>? {
|
||||
return map.getOrElse(c) { null } //as? Class<T>
|
||||
operator fun <T : Class<*>>get(c: Class<*>): T {
|
||||
return map.getOrElse(c) { throw IllegalArgumentException() } as T
|
||||
}
|
||||
}
|
||||
|
@@ -1,399 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
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.annotation.Nullable;
|
||||
import androidx.collection.ArrayMap;
|
||||
|
||||
import static com.topjohnwu.magisk.ConfigLeanback.getPrefs;
|
||||
import static com.topjohnwu.magisk.utils.XAndroidKt.getPackageName;
|
||||
|
||||
public final class Config {
|
||||
|
||||
private static final ArrayMap<String, Object> defs = new ArrayMap<>();
|
||||
public static int magiskVersionCode = -1;
|
||||
// Current status
|
||||
public static String magiskVersionString = "";
|
||||
// Update Info
|
||||
public static String remoteMagiskVersionString = "";
|
||||
public static int remoteMagiskVersionCode = -1;
|
||||
public static String magiskLink = "";
|
||||
public static String magiskNoteLink = "";
|
||||
public static String magiskMD5 = "";
|
||||
public static String remoteManagerVersionString = "";
|
||||
public static int remoteManagerVersionCode = -1;
|
||||
public static String managerLink = "";
|
||||
public static String managerNoteLink = "";
|
||||
public static String uninstallerLink = "";
|
||||
|
||||
// Install flags
|
||||
public static boolean keepVerity = false;
|
||||
public static boolean keepEnc = false;
|
||||
public static boolean recovery = false;
|
||||
|
||||
public static 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;
|
||||
}
|
||||
|
||||
private static boolean magiskHide = false;
|
||||
|
||||
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
|
||||
getPrefs().edit().apply();
|
||||
Context context = ConfigLeanback.getProtectedContext();
|
||||
File xml = new File(context.getFilesDir().getParent() + "/shared_prefs",
|
||||
getPackageName() + "_preferences.xml");
|
||||
Shell.su(Utils.fmt("cat %s > /data/adb/%s", xml, Const.MANAGER_CONFIGS)).exec();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
SharedPreferences pref = 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();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T get(String key) {
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
return (T) (Integer) getPrefs().getInt(key, getDef(key));
|
||||
case PREF_STR_INT:
|
||||
return (T) (Integer) Utils.getPrefsInt(getPrefs(), key, getDef(key));
|
||||
case PREF_BOOL:
|
||||
return (T) (Boolean) getPrefs().getBoolean(key, getDef(key));
|
||||
case PREF_STR:
|
||||
return (T) getPrefs().getString(key, getDef(key));
|
||||
case DB_INT:
|
||||
return (T) (Integer) ConfigLeanback.get(key, (Integer) getDef(key));
|
||||
case DB_BOOL:
|
||||
return (T) (Boolean) (ConfigLeanback.get(key, getDef(key) ? 1 : 0) != 0);
|
||||
case DB_STR:
|
||||
return (T) ConfigLeanback.get(key, getDef(key));
|
||||
}
|
||||
/* Will never get here (IllegalArgumentException in getConfigType) */
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void set(String key, Object val) {
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
getPrefs().edit().putInt(key, (int) val).apply();
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
getPrefs().edit().putString(key, String.valueOf(val)).apply();
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
getPrefs().edit().putBoolean(key, (boolean) val).apply();
|
||||
break;
|
||||
case PREF_STR:
|
||||
getPrefs().edit().putString(key, (String) val).apply();
|
||||
break;
|
||||
case DB_INT:
|
||||
ConfigLeanback.put(key, (int) val);
|
||||
break;
|
||||
case DB_BOOL:
|
||||
ConfigLeanback.put(key, (boolean) val ? 1 : 0);
|
||||
break;
|
||||
case DB_STR:
|
||||
ConfigLeanback.put(key, (String) val);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void remove(String key) {
|
||||
switch (getConfigType(key)) {
|
||||
case PREF_INT:
|
||||
case PREF_STR_INT:
|
||||
case PREF_BOOL:
|
||||
case PREF_STR:
|
||||
getPrefs().edit().remove(key).apply();
|
||||
break;
|
||||
case DB_BOOL:
|
||||
case DB_INT:
|
||||
ConfigLeanback.delete(key);
|
||||
break;
|
||||
case DB_STR:
|
||||
ConfigLeanback.put(key, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
private static <T> T getDef(String key) throws IllegalArgumentException {
|
||||
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) {
|
||||
for (String key : defs.keySet()) {
|
||||
Object value = defs.get(key);
|
||||
int type = getConfigType(key);
|
||||
switch (type) {
|
||||
case DB_INT:
|
||||
editor.putString(key, String.valueOf(ConfigLeanback.get(key, (Integer) value)));
|
||||
continue;
|
||||
case DB_STR:
|
||||
editor.putString(key, ConfigLeanback.get(key, String.valueOf(value)));
|
||||
continue;
|
||||
case DB_BOOL:
|
||||
int bs = ConfigLeanback.get(key, -1);
|
||||
editor.putBoolean(key, bs < 0 ? (Boolean) value : bs != 0);
|
||||
continue;
|
||||
}
|
||||
if (pref.contains(key))
|
||||
continue;
|
||||
switch (type) {
|
||||
case PREF_INT:
|
||||
editor.putInt(key, (Integer) value);
|
||||
break;
|
||||
case PREF_STR_INT:
|
||||
editor.putString(key, String.valueOf(value));
|
||||
break;
|
||||
case PREF_STR:
|
||||
editor.putString(key, (String) value);
|
||||
break;
|
||||
case PREF_BOOL:
|
||||
editor.putBoolean(key, (Boolean) value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
208
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
208
app/src/main/java/com/topjohnwu/magisk/Config.kt
Normal file
@@ -0,0 +1,208 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Environment
|
||||
import android.util.Xml
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.FingerprintHelper
|
||||
import com.topjohnwu.magisk.utils.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 REPO_ORDER = "repo_order"
|
||||
const val SHOW_SYSTEM_APP = "show_system"
|
||||
const val DOWNLOAD_PATH = "download_path"
|
||||
|
||||
// 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 downloadPath by preference(Key.DOWNLOAD_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
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)
|
||||
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, "")
|
||||
|
||||
var rootMode by dbSettings(Key.ROOT_ACCESS, Value.ROOT_ACCESS_APPS_AND_ADB)
|
||||
var suMntNamespaceMode by dbSettings(Key.SU_MNT_NS, Value.NAMESPACE_MODE_REQUESTER)
|
||||
var suMultiuserMode by dbSettings(Key.SU_MULTIUSER_MODE, Value.MULTIUSER_MODE_OWNER_ONLY)
|
||||
var suFingerprint by dbSettings(Key.SU_FINGERPRINT, false)
|
||||
var suManager by dbStrings(Key.SU_MANAGER, "", true)
|
||||
|
||||
// Always return a path in external storage where we can write
|
||||
val downloadDirectory get() =
|
||||
Utils.ensureDownloadPath(downloadPath) ?: get<Context>().getExternalFilesDir(null)!!
|
||||
|
||||
fun initialize() = prefs.edit {
|
||||
parsePrefs(this)
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
private fun parsePrefs(editor: SharedPreferences.Editor) = editor.apply {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config).buffered()
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.START_TAG, null, "map")
|
||||
while (parser.next() != XmlPullParser.END_TAG) {
|
||||
if (parser.eventType != XmlPullParser.START_TAG)
|
||||
continue
|
||||
val key: String = parser.getAttributeValue(null, "name")
|
||||
fun value() = parser.getAttributeValue(null, "value")!!
|
||||
when (parser.name) {
|
||||
"string" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "string")
|
||||
putString(key, parser.nextText())
|
||||
parser.require(XmlPullParser.END_TAG, null, "string")
|
||||
}
|
||||
"boolean" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "boolean")
|
||||
putBoolean(key, value().toBoolean())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "boolean")
|
||||
}
|
||||
"int" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putInt(key, value().toInt())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
"long" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "long")
|
||||
putLong(key, value().toLong())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "long")
|
||||
}
|
||||
"float" -> {
|
||||
parser.require(XmlPullParser.START_TAG, null, "int")
|
||||
putFloat(key, value().toFloat())
|
||||
parser.nextTag()
|
||||
parser.require(XmlPullParser.END_TAG, null, "int")
|
||||
}
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().commit()
|
||||
val xml = File(
|
||||
"${get<Context>(Protected).filesDir.parent}/shared_prefs",
|
||||
"${packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.data.repository.SettingRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
|
||||
object ConfigLeanback {
|
||||
|
||||
@JvmStatic
|
||||
val protectedContext: Context by inject(Protected)
|
||||
@JvmStatic
|
||||
val prefs: SharedPreferences by inject()
|
||||
|
||||
private val settingRepo: SettingRepository by inject()
|
||||
private val stringRepo: StringRepository by inject()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun put(key: String, value: Int) {
|
||||
settingRepo.put(key, value).subscribeK()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun get(key: String, defaultValue: Int): Int =
|
||||
settingRepo.fetch(key, defaultValue).blockingGet()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun put(key: String, value: String?) {
|
||||
val task = value?.let { stringRepo.put(key, it) } ?: stringRepo.delete(key)
|
||||
task.subscribeK()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun get(key: String, defaultValue: String?): String =
|
||||
stringRepo.fetch(key, defaultValue.orEmpty()).blockingGet()
|
||||
|
||||
@JvmStatic
|
||||
@AnyThread
|
||||
fun delete(key: String) {
|
||||
settingRepo.delete(key).subscribeK()
|
||||
}
|
||||
|
||||
}
|
@@ -1,44 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Environment
|
||||
import android.os.Process
|
||||
|
||||
import java.io.File
|
||||
|
||||
object Const {
|
||||
|
||||
const val DEBUG_TAG = "MagiskManager"
|
||||
|
||||
// APK content
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
const val SU_KEYSTORE_KEY = "su_key"
|
||||
|
||||
// Paths
|
||||
const val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
@JvmField
|
||||
val EXTERNAL_PATH: File =
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
@JvmField
|
||||
var MAGISK_DISABLE_FILE: File = File("xxx")
|
||||
|
||||
var MAGISK_DISABLE_FILE = File("xxx")
|
||||
const val TMP_FOLDER_PATH = "/dev/tmp"
|
||||
const val MAGISK_LOG = "/cache/magisk.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
|
||||
// Versions
|
||||
const val UPDATE_SERVICE_VER = 1
|
||||
const val SNET_EXT_VER = 12
|
||||
const val SNET_EXT_VER = 13
|
||||
const val SNET_REVISION = "5adbc435ce93ded953c30ebe587edfd50b5503bc"
|
||||
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
|
||||
|
||||
@JvmField
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
// Generic
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
|
||||
init {
|
||||
EXTERNAL_PATH.mkdirs()
|
||||
}
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object MagiskVersion {
|
||||
const val MIN_SUPPORT = 18000
|
||||
@@ -59,36 +41,28 @@ object Const {
|
||||
}
|
||||
|
||||
object Url {
|
||||
@Deprecated("This shouldn't be used. There's literally no need for it")
|
||||
const val REPO_URL =
|
||||
"https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&sort=pushed&page=%d"
|
||||
const val FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s"
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val MODULE_INSTALLER =
|
||||
"https://raw.githubusercontent.com/topjohnwu/Magisk/master/scripts/module_installer.sh"
|
||||
const val PAYPAL_URL = "https://www.paypal.me/topjohnwu"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val TWITTER_URL = "https://twitter.com/topjohnwu"
|
||||
const val XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
@JvmField
|
||||
val SNET_URL = getRaw("b66b1a914978e5f4c4bbfd74a59f4ad371bac107", "snet.apk")
|
||||
@JvmField
|
||||
val BOOTCTL_URL = getRaw("9c5dfc1b8245c0b5b524901ef0ff0f8335757b77", "bootctl")
|
||||
|
||||
private fun getRaw(where: String, name: String) =
|
||||
"https://raw.githubusercontent.com/topjohnwu/magisk_files/%s/%s".format(where, name)
|
||||
const val GITHUB_RAW_URL = "https://raw.githubusercontent.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/users/Magisk-Modules-Repo/"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val IF_NONE_MATCH = "If-None-Match"
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val INTENT_SET_NAME = "filename"
|
||||
const val INTENT_SET_LINK = "link"
|
||||
const val INTENT_SET_APP = "app_json"
|
||||
const val FLASH_ACTION = "action"
|
||||
const val FLASH_DATA = "additional_data"
|
||||
const val DISMISS_ID = "dismiss_id"
|
||||
const val BROADCAST_MANAGER_UPDATE = "manager_update"
|
||||
const val BROADCAST_REBOOT = "reboot"
|
||||
}
|
||||
@@ -101,5 +75,4 @@ object Const {
|
||||
const val UNINSTALL = "uninstall"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,20 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.os.Process
|
||||
|
||||
object Constants {
|
||||
|
||||
// Paths
|
||||
val MAGISK_PATH = "/sbin/.magisk/img"
|
||||
val MAGISK_LOG = "/cache/magisk.log"
|
||||
|
||||
val USER_ID get() = Process.myUid() / 100000
|
||||
|
||||
const val SNET_REVISION = "b66b1a914978e5f4c4bbfd74a59f4ad371bac107"
|
||||
const val BOOTCTL_REVISION = "9c5dfc1b8245c0b5b524901ef0ff0f8335757b77"
|
||||
|
||||
const val GITHUB_URL = "https://github.com/"
|
||||
const val GITHUB_API_URL = "https://api.github.com/"
|
||||
const val GITHUB_RAW_API_URL = "https://raw.githubusercontent.com/"
|
||||
|
||||
}
|
26
app/src/main/java/com/topjohnwu/magisk/Info.kt
Normal file
26
app/src/main/java/com/topjohnwu/magisk/Info.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
|
||||
object Info {
|
||||
|
||||
var magiskVersionCode = -1
|
||||
|
||||
var magiskVersionString = ""
|
||||
|
||||
var remote = UpdateInfo()
|
||||
|
||||
var keepVerity = false
|
||||
var keepEnc = false
|
||||
var recovery = false
|
||||
|
||||
fun loadMagiskInfo() {
|
||||
runCatching {
|
||||
magiskVersionString = ShellUtils.fastCmd("magisk -v").split(":".toRegex())[0]
|
||||
magiskVersionCode = ShellUtils.fastCmd("magisk -V").toInt()
|
||||
Config.magiskHide = Shell.su("magiskhide --status").exec().isSuccess
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,47 +0,0 @@
|
||||
package com.topjohnwu.magisk
|
||||
|
||||
import android.content.Context
|
||||
import com.topjohnwu.magisk.KConfig.UpdateChannel.STABLE
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.model.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
|
||||
object KConfig : PreferenceModel() {
|
||||
|
||||
override val context: Context by inject(Protected)
|
||||
override val fileName: String = "${context.packageName}_preferences"
|
||||
|
||||
private var internalUpdateChannel by preference(Config.Key.UPDATE_CHANNEL, STABLE.id.toString())
|
||||
var useCustomTabs by preference("useCustomTabs", true)
|
||||
@JvmStatic
|
||||
var customUpdateChannel by preference(Config.Key.CUSTOM_CHANNEL, "")
|
||||
|
||||
@JvmStatic
|
||||
var updateChannel: UpdateChannel
|
||||
get() = UpdateChannel.byId(internalUpdateChannel.toIntOrNull() ?: STABLE.id)
|
||||
set(value) {
|
||||
internalUpdateChannel = value.id.toString()
|
||||
}
|
||||
|
||||
internal const val DEFAULT_CHANNEL = "topjohnwu/magisk_files"
|
||||
|
||||
enum class UpdateChannel(val id: Int) {
|
||||
|
||||
STABLE(Config.Value.STABLE_CHANNEL),
|
||||
BETA(Config.Value.BETA_CHANNEL),
|
||||
CANARY(Config.Value.CANARY_CHANNEL),
|
||||
CANARY_DEBUG(Config.Value.CANARY_DEBUG_CHANNEL),
|
||||
CUSTOM(Config.Value.CUSTOM_CHANNEL);
|
||||
|
||||
companion object {
|
||||
fun byId(id: Int) = when (id) {
|
||||
Config.Value.STABLE_CHANNEL -> STABLE
|
||||
Config.Value.BETA_CHANNEL -> BETA
|
||||
Config.Value.CUSTOM_CHANNEL -> CUSTOM
|
||||
Config.Value.CANARY_CHANNEL -> CANARY
|
||||
Config.Value.CANARY_DEBUG_CHANNEL -> CANARY_DEBUG
|
||||
else -> STABLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.toLog
|
||||
@@ -12,7 +11,7 @@ class LogDao : BaseDao() {
|
||||
override val table = DatabaseDefinition.Table.LOG
|
||||
|
||||
fun deleteOutdated(
|
||||
suTimeout: Long = Config.suLogTimeout * TimeUnit.DAYS.toMillis(1)
|
||||
suTimeout: Long = TimeUnit.DAYS.toMillis(14)
|
||||
) = query<Delete> {
|
||||
condition {
|
||||
lessThan("time", suTimeout.toString())
|
||||
|
@@ -2,12 +2,13 @@ package com.topjohnwu.magisk.data.database
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.database.base.*
|
||||
import com.topjohnwu.magisk.extensions.now
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.toMap
|
||||
import com.topjohnwu.magisk.model.entity.toPolicy
|
||||
import com.topjohnwu.magisk.utils.now
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@@ -47,12 +48,7 @@ class PolicyDao(
|
||||
condition {
|
||||
equals("uid", uid)
|
||||
}
|
||||
}.map { it.firstOrNull()?.toPolicy(context.packageManager) }
|
||||
.doOnError {
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
}
|
||||
}.map { it.first().toPolicySafe() }
|
||||
|
||||
fun update(policy: MagiskPolicy) = query<Replace> {
|
||||
values(policy.toMap())
|
||||
@@ -60,10 +56,20 @@ class PolicyDao(
|
||||
|
||||
fun fetchAll() = query<Select> {
|
||||
condition {
|
||||
equals("uid/100000", Constants.USER_ID)
|
||||
equals("uid/100000", Const.USER_ID)
|
||||
}
|
||||
}.flattenAsFlowable { it }
|
||||
.map { it.toPolicy(context.packageManager) }
|
||||
.toList()
|
||||
}.map { it.mapNotNull { it.toPolicySafe() } }
|
||||
|
||||
|
||||
private fun Map<String, String>.toPolicySafe(): MagiskPolicy? {
|
||||
return runCatching { toPolicy(context.packageManager) }.getOrElse {
|
||||
Timber.e(it)
|
||||
if (it is PackageManager.NameNotFoundException) {
|
||||
val uid = getOrElse("uid") { null } ?: return null
|
||||
delete(uid).subscribe()
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
val repos: List<Repo> get() = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getReposNameOrder()
|
||||
else -> getReposDateOrder()
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(etag) = addEtagRaw(RepoEtag(0, etag))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
fun clear() {
|
||||
clearRepos()
|
||||
clearEtag()
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE")
|
||||
protected abstract fun getReposNameOrder(): List<Repo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun addRepo(repo: Repo)
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id")
|
||||
abstract fun getRepo(id: String): Repo?
|
||||
|
||||
@Query("SELECT id FROM repos")
|
||||
protected abstract fun getRepoID(): List<RepoID>
|
||||
|
||||
@Delete
|
||||
abstract fun removeRepo(repo: Repo)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id = :id")
|
||||
abstract fun removeRepo(id: String)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id IN (:idList)")
|
||||
abstract fun removeRepos(idList: Collection<String>)
|
||||
|
||||
@Query("SELECT * FROM etag")
|
||||
protected abstract fun etagRaw(): RepoEtag?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
|
||||
@Query("DELETE FROM repos")
|
||||
protected abstract fun clearRepos()
|
||||
|
||||
@Query("DELETE FROM etag")
|
||||
protected abstract fun clearEtag()
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
@PrimaryKey val id: String
|
||||
)
|
||||
|
||||
@Entity(tableName = "etag")
|
||||
data class RepoEtag(
|
||||
@PrimaryKey val id: Int,
|
||||
val key: String
|
||||
)
|
||||
|
@@ -0,0 +1,11 @@
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class])
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
}
|
@@ -1,118 +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;
|
||||
|
||||
@Deprecated
|
||||
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final int DATABASE_VER = 5;
|
||||
private static final String TABLE_NAME = "repos";
|
||||
|
||||
private final SQLiteDatabase mDb;
|
||||
|
||||
@Deprecated
|
||||
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);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void clearRepo() {
|
||||
mDb.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(String id) {
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(Repo repo) {
|
||||
removeRepo(repo.getId());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void removeRepo(Iterable<String> list) {
|
||||
for (String id : list) {
|
||||
if (id == null) continue;
|
||||
mDb.delete(TABLE_NAME, "id=?", new String[]{id});
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void addRepo(Repo repo) {
|
||||
mDb.replace(TABLE_NAME, null, repo.getContentValues());
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
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;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public Cursor getRawCursor() {
|
||||
return mDb.query(TABLE_NAME, null, null, null, null, null, null);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
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);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
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;
|
||||
}
|
||||
}
|
@@ -10,11 +10,12 @@ class SettingsDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: Int) = query<Insert> {
|
||||
values(key to value.toString())
|
||||
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 }
|
||||
|
||||
|
@@ -10,8 +10,8 @@ class StringDao : BaseDao() {
|
||||
condition { equals("key", key) }
|
||||
}.ignoreElement()
|
||||
|
||||
fun put(key: String, value: String) = query<Insert> {
|
||||
values(key to value)
|
||||
fun put(key: String, value: String) = query<Replace> {
|
||||
values("key" to key, "value" to value)
|
||||
}.ignoreElement()
|
||||
|
||||
fun fetch(key: String, default: String = "") = query<Select> {
|
||||
|
@@ -32,11 +32,7 @@ class Delete : MagiskQueryBuilder {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln(condition)
|
||||
.toString()
|
||||
return listOf(requestType, table, condition).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +61,7 @@ class Select : MagiskQueryBuilder {
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln(condition)
|
||||
.appendln(orderField)
|
||||
.toString()
|
||||
.replace("\n", " ")
|
||||
return listOf(requestType, table, condition, orderField).joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,23 +74,25 @@ open class Insert : MagiskQueryBuilder {
|
||||
override lateinit var table: String
|
||||
|
||||
private val keys get() = _values.keys.joinToString(",")
|
||||
private val values get() = _values.values.joinToString(",") { "\"$it\"" }
|
||||
private var _values: Map<String, String> = mapOf()
|
||||
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, String>) {
|
||||
fun values(vararg pairs: Pair<String, Any>) {
|
||||
_values = pairs.toMap()
|
||||
}
|
||||
|
||||
fun values(values: Map<String, String>) {
|
||||
fun values(values: Map<String, Any>) {
|
||||
_values = values
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return StringBuilder()
|
||||
.appendln(requestType)
|
||||
.appendln(table)
|
||||
.appendln("($keys) VALUES($values)")
|
||||
.toString()
|
||||
return listOf(requestType, table, "($keys) VALUES($values)").joinToString(" ")
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,63 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.KConfig
|
||||
import com.topjohnwu.magisk.model.entity.MagiskConfig
|
||||
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 fetchConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/beta.json")
|
||||
fun fetchBetaConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/release.json")
|
||||
fun fetchCanaryConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/master/canary_builds/canary.json")
|
||||
fun fetchCanaryDebugConfig(): Single<MagiskConfig>
|
||||
|
||||
@GET
|
||||
fun fetchCustomConfig(@Url url: String): Single<MagiskConfig>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/snet.apk")
|
||||
@Streaming
|
||||
fun fetchSafetynet(@Path(REVISION) revision: String = Constants.SNET_REVISION): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_FILES/{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
fun fetchBootctl(@Path(REVISION) revision: String = Constants.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 = KConfig.DEFAULT_CHANNEL
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.model.entity.UpdateInfo
|
||||
import com.topjohnwu.magisk.tasks.GithubRepoInfo
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.adapter.rxjava2.Result
|
||||
import retrofit2.http.*
|
||||
|
||||
interface GithubRawServices {
|
||||
|
||||
//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.jar")
|
||||
@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>
|
||||
|
||||
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
fun fetchInstaller(): Single<ResponseBody>
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
fun fetchModuleInfo(@Path(MODULE) id: String, @Path(FILE) file: String): Single<String>
|
||||
|
||||
//endregion
|
||||
|
||||
/**
|
||||
* 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>
|
||||
|
||||
@GET
|
||||
fun fetchString(@Url url: String): Single<String>
|
||||
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface GithubApiServices {
|
||||
|
||||
@GET("repos")
|
||||
fun fetchRepos(@Query("page") page: Int,
|
||||
@Header(Const.Key.IF_NONE_MATCH) etag: String,
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100): Flowable<Result<List<GithubRepoInfo>>>
|
||||
|
||||
}
|
@@ -1,15 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
|
||||
class AppRepository(private val policyDao: PolicyDao) {
|
||||
|
||||
fun deleteOutdated() = policyDao.deleteOutdated()
|
||||
fun delete(packageName: String) = policyDao.delete(packageName)
|
||||
fun delete(uid: Int) = policyDao.delete(uid)
|
||||
fun fetch(uid: Int) = policyDao.fetch(uid)
|
||||
fun fetchAll() = policyDao.fetchAll()
|
||||
fun update(policy: MagiskPolicy) = policyDao.update(policy)
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
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,
|
||||
sync: Boolean = false
|
||||
) = DBStringsValue(name, default, sync)
|
||||
|
||||
}
|
||||
|
||||
class DBSettingsValue(
|
||||
private val name: String,
|
||||
private val default: Int
|
||||
) : ReadWriteProperty<DBConfig, Int> {
|
||||
|
||||
private var value: Int? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): Int {
|
||||
if (value == null)
|
||||
value = thisRef.settingsDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: Int) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
thisRef.settingsDao.put(name, 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,
|
||||
private val sync: Boolean
|
||||
) : ReadWriteProperty<DBConfig, String> {
|
||||
|
||||
private var value: String? = null
|
||||
|
||||
@Synchronized
|
||||
override fun getValue(thisRef: DBConfig, property: KProperty<*>): String {
|
||||
if (value == null)
|
||||
value = thisRef.stringDao.fetch(name, default).blockingGet()
|
||||
return value!!
|
||||
}
|
||||
|
||||
override fun setValue(thisRef: DBConfig, property: KProperty<*>, value: String) {
|
||||
synchronized(this) {
|
||||
this.value = value
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
if (sync) {
|
||||
thisRef.stringDao.delete(name).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.delete(name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
} else {
|
||||
if (sync) {
|
||||
thisRef.stringDao.put(name, value).blockingAwait()
|
||||
} else {
|
||||
thisRef.stringDao.put(name, value)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +1,12 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.data.database.LogDao
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.toSingle
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
@@ -20,9 +18,8 @@ class LogRepository(
|
||||
.map { it.sortByDescending { it.date.time }; it }
|
||||
.map { it.wrap() }
|
||||
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Constants.MAGISK_LOG}".suRaw()
|
||||
fun fetchMagiskLogs() = "tail -n 5000 ${Const.MAGISK_LOG}".suRaw()
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { Timber.i(it.toString()); it }
|
||||
|
||||
fun clearLogs() = logDao.deleteAll()
|
||||
fun clearOutdated() = logDao.deleteOutdated()
|
||||
|
@@ -1,86 +1,49 @@
|
||||
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.KConfig
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.database.base.suRaw
|
||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.extensions.packageName
|
||||
import com.topjohnwu.magisk.extensions.toSingle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.Version
|
||||
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
|
||||
import io.reactivex.functions.BiFunction
|
||||
|
||||
class MagiskRepository(
|
||||
private val context: Context,
|
||||
private val apiRaw: GithubRawApiServices,
|
||||
private val packageManager: PackageManager
|
||||
private val apiRaw: GithubRawServices,
|
||||
private val packageManager: PackageManager
|
||||
) {
|
||||
|
||||
fun fetchMagisk() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.magisk.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_ZIP) }
|
||||
fun fetchSafetynet() = apiRaw.fetchSafetynet()
|
||||
|
||||
fun fetchManager() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.app.link) }
|
||||
.map { it.writeToFile(context, FILE_MAGISK_APK) }
|
||||
|
||||
fun fetchUninstaller() = fetchConfig()
|
||||
.flatMap { apiRaw.fetchFile(it.uninstaller.link) }
|
||||
.map { it.writeToFile(context, FILE_UNINSTALLER_ZIP) }
|
||||
|
||||
fun fetchSafetynet() = apiRaw
|
||||
.fetchSafetynet()
|
||||
.map { it.writeToFile(context, FILE_SAFETY_NET_APK) }
|
||||
|
||||
fun fetchBootctl() = apiRaw
|
||||
.fetchBootctl()
|
||||
.map { it.writeToFile(context, FILE_BOOTCTL_SH) }
|
||||
|
||||
|
||||
fun fetchConfig() = when (KConfig.updateChannel) {
|
||||
KConfig.UpdateChannel.STABLE -> apiRaw.fetchConfig()
|
||||
KConfig.UpdateChannel.BETA -> apiRaw.fetchBetaConfig()
|
||||
KConfig.UpdateChannel.CANARY -> apiRaw.fetchCanaryConfig()
|
||||
KConfig.UpdateChannel.CANARY_DEBUG -> apiRaw.fetchCanaryDebugConfig()
|
||||
KConfig.UpdateChannel.CUSTOM -> apiRaw.fetchCustomConfig(KConfig.customUpdateChannel)
|
||||
}
|
||||
.doOnSuccess {
|
||||
Config.remoteMagiskVersionCode = it.magisk.versionCode.toIntOrNull() ?: -1
|
||||
Config.magiskLink = it.magisk.link
|
||||
Config.magiskNoteLink = it.magisk.note
|
||||
Config.magiskMD5 = it.magisk.hash
|
||||
Config.remoteManagerVersionCode = it.app.versionCode.toIntOrNull() ?: -1
|
||||
Config.remoteManagerVersionString = it.app.version
|
||||
Config.managerLink = it.app.link
|
||||
Config.managerNoteLink = it.app.note
|
||||
Config.uninstallerLink = it.uninstaller.link
|
||||
}
|
||||
|
||||
|
||||
fun fetchMagiskVersion(): Single<Version> = Single.zip(
|
||||
fetchMagiskVersionName(),
|
||||
fetchMagiskVersionCode(),
|
||||
BiFunction { versionName, versionCode ->
|
||||
Version(versionName, versionCode)
|
||||
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)
|
||||
}
|
||||
)
|
||||
|
||||
}.doOnSuccess { Info.remote = it }
|
||||
|
||||
fun fetchApps() =
|
||||
Single.fromCallable { packageManager.getInstalledApplications(0) }
|
||||
.flattenAsFlowable { it }
|
||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||
.map {
|
||||
val label = Utils.getAppLabel(it, packageManager)
|
||||
val label = it.getLabel(packageManager)
|
||||
val icon = it.loadIcon(packageManager)
|
||||
HideAppInfo(it, label, icon)
|
||||
}
|
||||
@@ -93,30 +56,14 @@ class MagiskRepository(
|
||||
.map { HideTarget(it) }
|
||||
.toList()
|
||||
|
||||
private fun fetchMagiskVersionName() = "magisk -v".suRaw()
|
||||
.map { it.first() }
|
||||
.map { it.substring(0 until it.indexOf(":")) }
|
||||
.onErrorReturn { "Unknown" }
|
||||
|
||||
private fun fetchMagiskVersionCode() = "magisk -V".suRaw()
|
||||
.map { it.first() }
|
||||
.map { it.toIntOrNull() ?: -1 }
|
||||
.onErrorReturn { -1 }
|
||||
|
||||
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,
|
||||
private val blacklist by lazy { listOf(
|
||||
packageName,
|
||||
"android",
|
||||
"com.android.chrome",
|
||||
"com.chrome.beta",
|
||||
@@ -124,7 +71,7 @@ class MagiskRepository(
|
||||
"com.chrome.canary",
|
||||
"com.android.webview",
|
||||
"com.google.android.webview"
|
||||
)
|
||||
) }
|
||||
}
|
||||
|
||||
}
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.SettingsDao
|
||||
|
||||
class SettingRepository(private val settingsDao: SettingsDao) {
|
||||
|
||||
fun fetch(key: String, default: Int) = settingsDao.fetch(key, default)
|
||||
fun put(key: String, value: Int) = settingsDao.put(key, value)
|
||||
fun delete(key: String) = settingsDao.delete(key)
|
||||
|
||||
}
|
@@ -1,11 +1,15 @@
|
||||
package com.topjohnwu.magisk.data.repository
|
||||
|
||||
import com.topjohnwu.magisk.data.database.StringDao
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
class StringRepository(private val stringDao: StringDao) {
|
||||
class StringRepository(
|
||||
private val api: GithubRawServices
|
||||
) {
|
||||
|
||||
fun fetch(key: String, default: String) = stringDao.fetch(key, default)
|
||||
fun put(key: String, value: String) = stringDao.put(key, value)
|
||||
fun delete(key: String) = stringDao.delete(key)
|
||||
fun getString(url: String) = api.fetchString(url)
|
||||
|
||||
fun getMetadata(repo: Repo) = api.fetchModuleInfo(repo.id, "module.prop")
|
||||
|
||||
fun getReadme(repo: Repo) = api.fetchModuleInfo(repo.id, "README.md")
|
||||
}
|
@@ -1,18 +1,61 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.topjohnwu.magisk.App
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.dsl.module
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
||||
|
||||
val applicationModule = module {
|
||||
single { RxBus() }
|
||||
factory { get<Context>().resources }
|
||||
factory { get<Context>() as App }
|
||||
factory { get<Context>().packageManager }
|
||||
factory(Protected) { get<App>().protectedContext }
|
||||
factory(Protected) { createDEContext(get()) }
|
||||
single(SUTimeout) { get<Context>(Protected).getSharedPreferences("su_timeout", 0) }
|
||||
single { PreferenceManager.getDefaultSharedPreferences(get<Context>(Protected)) }
|
||||
}
|
||||
single { ActivityTracker() }
|
||||
factory { get<ActivityTracker>().foreground ?: NullActivity }
|
||||
}
|
||||
|
||||
private fun createDEContext(context: Context): Context {
|
||||
return if (Build.VERSION.SDK_INT >= 24)
|
||||
context.createDeviceProtectedStorageContext()
|
||||
else context
|
||||
}
|
||||
|
||||
class ActivityTracker : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
@Volatile
|
||||
var foreground: Activity? = null
|
||||
|
||||
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
foreground = activity
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onActivityPaused(activity: Activity) {
|
||||
foreground = null
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {}
|
||||
}
|
||||
|
||||
@SuppressLint("Registered")
|
||||
object NullActivity : Activity()
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.topjohnwu.magisk.data.database.*
|
||||
import com.topjohnwu.magisk.tasks.UpdateRepos
|
||||
import com.topjohnwu.magisk.tasks.RepoUpdater
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
@@ -10,6 +12,12 @@ val databaseModule = module {
|
||||
single { PolicyDao(get()) }
|
||||
single { SettingsDao() }
|
||||
single { StringDao() }
|
||||
single { RepoDatabaseHelper(get()) }
|
||||
single { UpdateRepos(get()) }
|
||||
single { createRepoDatabase(get()) }
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
||||
fun createRepoDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, RepoDatabase::class.java, "repo.db")
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val miscModule = module {
|
||||
|
||||
// define miscs here
|
||||
|
||||
}
|
@@ -5,6 +5,5 @@ val koinModules = listOf(
|
||||
networkingModule,
|
||||
databaseModule,
|
||||
repositoryModule,
|
||||
viewModelModules,
|
||||
miscModule
|
||||
viewModelModules
|
||||
)
|
||||
|
@@ -1,6 +0,0 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import org.koin.core.qualifier.named
|
||||
|
||||
val SUTimeout = named("su_timeout")
|
||||
val Protected = named("protected")
|
@@ -2,64 +2,60 @@ package com.topjohnwu.magisk.di
|
||||
|
||||
import com.squareup.moshi.JsonAdapter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.Constants
|
||||
import com.topjohnwu.magisk.data.network.GithubRawApiServices
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import org.koin.dsl.module
|
||||
import retrofit2.CallAdapter
|
||||
import retrofit2.Converter
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
|
||||
import retrofit2.converter.moshi.MoshiConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
import se.ansman.kotshi.KotshiJsonAdapterFactory
|
||||
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient() }
|
||||
single { createConverterFactory() }
|
||||
single { createCallAdapterFactory() }
|
||||
single { createRetrofit(get(), get(), get()) }
|
||||
single { createApiService<GithubRawApiServices>(get(), Constants.GITHUB_RAW_API_URL) }
|
||||
single { createMoshiConverterFactory() }
|
||||
single { createRetrofit(get(), get()) }
|
||||
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||
}
|
||||
|
||||
fun createOkHttpClient(): OkHttpClient {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
val builder = OkHttpClient.Builder()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(httpLoggingInterceptor)
|
||||
.build()
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun createConverterFactory(): Converter.Factory {
|
||||
fun createMoshiConverterFactory(): MoshiConverterFactory {
|
||||
val moshi = Moshi.Builder()
|
||||
.add(JsonAdapterFactory.INSTANCE)
|
||||
.add(KotshiJsonAdapterFactory)
|
||||
.build()
|
||||
return MoshiConverterFactory.create(moshi)
|
||||
}
|
||||
|
||||
fun createCallAdapterFactory(): CallAdapter.Factory {
|
||||
return RxJava2CallAdapterFactory.create()
|
||||
}
|
||||
|
||||
fun createRetrofit(
|
||||
okHttpClient: OkHttpClient,
|
||||
converterFactory: Converter.Factory,
|
||||
callAdapterFactory: CallAdapter.Factory
|
||||
converterFactory: MoshiConverterFactory
|
||||
): Retrofit.Builder {
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(ScalarsConverterFactory.create())
|
||||
.addConverterFactory(converterFactory)
|
||||
.addCallAdapterFactory(callAdapterFactory)
|
||||
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
|
||||
.client(okHttpClient)
|
||||
}
|
||||
|
||||
@KotshiJsonAdapterFactory
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory {
|
||||
companion object {
|
||||
val INSTANCE: JsonAdapterFactory = KotshiJsonAdapterFactory
|
||||
}
|
||||
}
|
||||
abstract class JsonAdapterFactory : JsonAdapter.Factory
|
||||
|
||||
inline fun <reified T> createApiService(retrofitBuilder: Retrofit.Builder, baseUrl: String): T {
|
||||
return retrofitBuilder
|
||||
|
@@ -1,13 +1,13 @@
|
||||
package com.topjohnwu.magisk.di
|
||||
|
||||
import com.topjohnwu.magisk.data.repository.*
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
|
||||
val repositoryModule = module {
|
||||
single { MagiskRepository(get(), get(), get()) }
|
||||
single { MagiskRepository(get(), get()) }
|
||||
single { LogRepository(get()) }
|
||||
single { AppRepository(get()) }
|
||||
single { SettingRepository(get()) }
|
||||
single { StringRepository(get()) }
|
||||
}
|
||||
|
@@ -20,6 +20,8 @@ val viewModelModules = module {
|
||||
viewModel { HideViewModel(get(), get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { LogViewModel(get(), get()) }
|
||||
viewModel { (action: String, uri: Uri?) -> FlashViewModel(action, uri, get()) }
|
||||
viewModel { (action: String, file: Uri, additional: Uri) ->
|
||||
FlashViewModel(action, file, additional, get())
|
||||
}
|
||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -7,17 +7,16 @@ import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.res.Configuration
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.utils.FileProvider
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
val packageName: String
|
||||
get() {
|
||||
val app: App by inject()
|
||||
return app.packageName
|
||||
}
|
||||
val packageName: String get() = get<Context>().packageName
|
||||
|
||||
val PackageInfo.processes
|
||||
get() = activities?.processNames.orEmpty() +
|
||||
@@ -50,21 +49,22 @@ val ApplicationInfo.packageInfo: PackageInfo?
|
||||
}
|
||||
}
|
||||
|
||||
val Uri.fileName: String get() {
|
||||
var name: String? = null
|
||||
App.self.contentResolver.query(this, null, null, null, null)?.use { c ->
|
||||
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
c.moveToFirst()
|
||||
name = c.getString(nameIndex)
|
||||
val Uri.fileName: String
|
||||
get() {
|
||||
var name: String? = null
|
||||
get<Context>().contentResolver.query(this, null, null, null, null)?.use { c ->
|
||||
val nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
c.moveToFirst()
|
||||
name = c.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
if (name == null && path != null) {
|
||||
val idx = path!!.lastIndexOf('/')
|
||||
name = path!!.substring(idx + 1)
|
||||
}
|
||||
return name.orEmpty()
|
||||
}
|
||||
if (name == null && path != null) {
|
||||
val idx = path!!.lastIndexOf('/')
|
||||
name = path!!.substring(idx + 1)
|
||||
}
|
||||
return name.orEmpty()
|
||||
}
|
||||
|
||||
fun PackageManager.activities(packageName: String) =
|
||||
getPackageInfo(packageName, GET_ACTIVITIES)
|
||||
@@ -80,24 +80,42 @@ fun PackageManager.providers(packageName: String) =
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
fun Context.readUri(uri: Uri) = contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun ApplicationInfo.findAppLabel(pm: PackageManager): String {
|
||||
return pm.getApplicationLabel(this)?.toString().orEmpty()
|
||||
}
|
||||
fun Context.readUri(uri: Uri) =
|
||||
contentResolver.openInputStream(uri) ?: throw FileNotFoundException()
|
||||
|
||||
fun Intent.startActivity(context: Context) = context.startActivity(this)
|
||||
|
||||
fun File.provide(): Uri {
|
||||
val context: Context by inject()
|
||||
return FileProvider.getUriForFile(context, "com.topjohnwu.magisk.fileprovider", this)
|
||||
fun File.provide(context: Context = get()): Uri {
|
||||
return FileProvider.getUriForFile(context, context.packageName + ".provider", this)
|
||||
}
|
||||
|
||||
fun File.mv(destination: File) {
|
||||
inputStream().copyTo(destination)
|
||||
inputStream().writeTo(destination)
|
||||
deleteRecursively()
|
||||
}
|
||||
|
||||
fun String.toFile() = File(this)
|
||||
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||
fun Intent.chooser(title: String = "Pick an app") = Intent.createChooser(this, title)
|
||||
|
||||
fun Context.cachedFile(name: String) = File(cacheDir, name)
|
||||
|
||||
fun <Result> Cursor.toList(transformer: (Cursor) -> Result): List<Result> {
|
||||
val out = mutableListOf<Result>()
|
||||
while (moveToNext()) out.add(transformer(this))
|
||||
return out
|
||||
}
|
||||
|
||||
fun ApplicationInfo.getLabel(pm: PackageManager): String {
|
||||
runCatching {
|
||||
if (labelRes > 0) {
|
||||
val res = pm.getResourcesForApplication(this)
|
||||
val config = Configuration()
|
||||
config.setLocale(currentLocale)
|
||||
res.updateConfiguration(config, res.displayMetrics)
|
||||
return res.getString(labelRes)
|
||||
}
|
||||
}
|
||||
|
||||
return loadLabel(pm).toString()
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
|
103
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
103
app/src/main/java/com/topjohnwu/magisk/extensions/XJava.kt
Normal file
@@ -0,0 +1,103 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.net.toFile
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import kotlin.NoSuchElementException
|
||||
|
||||
fun ZipInputStream.forEach(callback: (ZipEntry) -> Unit) {
|
||||
var entry: ZipEntry? = nextEntry
|
||||
while (entry != null) {
|
||||
callback(entry)
|
||||
entry = nextEntry
|
||||
}
|
||||
}
|
||||
|
||||
fun Uri.writeTo(file: File) = toFile().copyTo(file)
|
||||
|
||||
fun InputStream.writeTo(file: File) =
|
||||
withStreams(this, file.outputStream()) { reader, writer -> reader.copyTo(writer) }
|
||||
|
||||
inline fun <In : InputStream, Out : OutputStream> withStreams(
|
||||
inStream: In,
|
||||
outStream: Out,
|
||||
withBoth: (In, Out) -> Unit
|
||||
) {
|
||||
inStream.use { reader ->
|
||||
outStream.use { writer ->
|
||||
withBoth(reader, writer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <T, R> List<T>.firstMap(mapper: (T) -> R?): R {
|
||||
for (item: T in this) {
|
||||
return mapper(item) ?: continue
|
||||
}
|
||||
throw NoSuchElementException("Collection contains no element matching the predicate.")
|
||||
}
|
||||
|
||||
fun String.langTagToLocale(): Locale {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return Locale.forLanguageTag(this)
|
||||
} else {
|
||||
val tok = split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (tok.isEmpty()) {
|
||||
return Locale("")
|
||||
}
|
||||
val language = when (tok[0]) {
|
||||
"und" -> "" // Undefined
|
||||
"fil" -> "tl" // Filipino
|
||||
else -> tok[0]
|
||||
}
|
||||
if (language.length != 2 && language.length != 3)
|
||||
return Locale("")
|
||||
if (tok.size == 1)
|
||||
return Locale(language)
|
||||
val country = tok[1]
|
||||
|
||||
return if (country.length != 2 && country.length != 3) Locale(language)
|
||||
else Locale(language, country)
|
||||
}
|
||||
}
|
||||
|
||||
fun Locale.toLangTag(): String {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
return toLanguageTag()
|
||||
} else {
|
||||
var language = language
|
||||
var country = country
|
||||
var variant = variant
|
||||
when {
|
||||
language.isEmpty() || !language.matches("\\p{Alpha}{2,8}".toRegex()) ->
|
||||
language = "und" // Follow the Locale#toLanguageTag() implementation
|
||||
language == "iw" -> language = "he" // correct deprecated "Hebrew"
|
||||
language == "in" -> language = "id" // correct deprecated "Indonesian"
|
||||
language == "ji" -> language = "yi" // correct deprecated "Yiddish"
|
||||
}
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
// ensure valid country code, if not well formed, it's omitted
|
||||
if (!country.matches("\\p{Alpha}{2}|\\p{Digit}{3}".toRegex())) {
|
||||
country = ""
|
||||
}
|
||||
|
||||
// variant subtags that begin with a letter must be at least 5 characters long
|
||||
if (!variant.matches("\\p{Alnum}{5,8}|\\p{Digit}\\p{Alnum}{3}".toRegex())) {
|
||||
variant = ""
|
||||
}
|
||||
val tag = StringBuilder(language)
|
||||
if (country.isNotEmpty())
|
||||
tag.append('-').append(country)
|
||||
if (variant.isNotEmpty())
|
||||
tag.append('-').append(variant)
|
||||
return tag.toString()
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import org.koin.core.context.GlobalContext
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
@@ -6,12 +6,12 @@ import org.koin.core.qualifier.Qualifier
|
||||
|
||||
fun getKoin() = GlobalContext.get().koin
|
||||
|
||||
inline fun <reified T : Any> inject(
|
||||
inline fun <reified T> inject(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
) = lazy { get<T>(qualifier, parameters) }
|
||||
|
||||
inline fun <reified T : Any> get(
|
||||
inline fun <reified T> get(
|
||||
qualifier: Qualifier? = null,
|
||||
noinline parameters: ParametersDefinition? = null
|
||||
): T = getKoin().get(qualifier, parameters)
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import androidx.databinding.ObservableList
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
@@ -76,4 +77,8 @@ fun <T1> ObservableList<T1>.copyNewInputInto(
|
||||
val addedValues = sender?.slice(positionStart until positionEnd).orEmpty()
|
||||
target.addAll(addedValues)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
operator fun <E> SparseArrayCompat<E>.set(key: Int, value: E) {
|
||||
put(key, value)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.BiFunction
|
14
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
Normal file
14
app/src/main/java/com/topjohnwu/magisk/extensions/XSU.kt
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream
|
||||
import java.io.File
|
||||
|
||||
fun reboot(reason: String = if (Info.recovery) "recovery" else "") {
|
||||
Shell.su("/system/bin/svc power reboot $reason || /system/bin/reboot $reason").submit()
|
||||
}
|
||||
|
||||
fun File.suOutputStream() = SuFileOutputStream(this)
|
||||
fun File.suInputStream() = SuFileInputStream(this)
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.content.res.Resources
|
||||
|
||||
@@ -20,4 +20,8 @@ fun Int.res(vararg args: Any): String {
|
||||
return resources.getString(this, *args)
|
||||
}
|
||||
|
||||
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
||||
fun String.trimEmptyToNull(): String? = if (isBlank()) null else this
|
||||
|
||||
fun String.legalFilename() = replace(" ", "_").replace("'", "").replace("\"", "")
|
||||
.replace("$", "").replace("`", "").replace("*", "").replace("/", "_")
|
||||
.replace("#", "").replace("@", "").replace("\\", "_")
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import com.topjohnwu.magisk.utils.currentLocale
|
||||
import java.text.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -13,8 +14,7 @@ fun String.toTime(format: DateFormat) = try {
|
||||
-1L
|
||||
}
|
||||
|
||||
private val locale get() = LocaleManager.locale
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", locale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", locale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.locale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", LocaleManager.locale) }
|
||||
val timeFormatFull by lazy { SimpleDateFormat("yyyy/MM/dd_HH:mm:ss", currentLocale) }
|
||||
val timeFormatStandard by lazy { SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", currentLocale) }
|
||||
val timeFormatMedium by lazy { DateFormat.getDateInstance(DateFormat.MEDIUM, currentLocale) }
|
||||
val timeFormatTime by lazy { SimpleDateFormat("h:mm a", currentLocale) }
|
@@ -1,4 +1,4 @@
|
||||
package com.topjohnwu.magisk.utils
|
||||
package com.topjohnwu.magisk.extensions
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
@@ -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
|
||||
}
|
||||
|
||||
}
|
@@ -1,156 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.download;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.ClassMap;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.model.entity.Repo;
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity;
|
||||
import com.topjohnwu.magisk.view.Notifications;
|
||||
import com.topjohnwu.magisk.view.ProgressNotification;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class DownloadModuleService extends Service {
|
||||
|
||||
private List<ProgressNotification> notifications;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
notifications = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Shell.EXECUTOR.execute(() -> {
|
||||
Repo repo = intent.getParcelableExtra("repo");
|
||||
boolean install = intent.getBooleanExtra("install", false);
|
||||
dlProcessInstall(repo, install);
|
||||
});
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onTaskRemoved(Intent rootIntent) {
|
||||
for (ProgressNotification n : notifications) {
|
||||
Notifications.mgr.cancel(n.hashCode());
|
||||
}
|
||||
notifications.clear();
|
||||
}
|
||||
|
||||
private synchronized void addNotification(ProgressNotification n) {
|
||||
if (notifications.isEmpty()) {
|
||||
// Start foreground
|
||||
startForeground(n.hashCode(), n.getNotification());
|
||||
}
|
||||
notifications.add(n);
|
||||
}
|
||||
|
||||
private synchronized void removeNotification(ProgressNotification n) {
|
||||
notifications.remove(n);
|
||||
if (notifications.isEmpty()) {
|
||||
// No more tasks, stop service
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
} else {
|
||||
// Pick another notification as our foreground notification
|
||||
n = notifications.get(0);
|
||||
startForeground(n.hashCode(), n.getNotification());
|
||||
}
|
||||
}
|
||||
|
||||
private void dlProcessInstall(Repo repo, boolean install) {
|
||||
File output = new File(Const.EXTERNAL_PATH, repo.getDownloadFilename());
|
||||
ProgressNotification progress = new ProgressNotification(output.getName());
|
||||
addNotification(progress);
|
||||
try {
|
||||
InputStream in = Networking.get(repo.getZipUrl())
|
||||
.setDownloadProgressListener(progress)
|
||||
.execForInputStream().getResult();
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(output));
|
||||
processZip(in, out);
|
||||
Intent intent = new Intent(this, ClassMap.get(FlashActivity.class));
|
||||
intent.setData(Uri.fromFile(output))
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(Const.Key.FLASH_ACTION, Const.Value.FLASH_ZIP);
|
||||
synchronized (getApplication()) {
|
||||
if (install && App.foreground() != null &&
|
||||
!(App.foreground() instanceof FlashActivity)) {
|
||||
/* Only start flashing if there is a foreground activity and the
|
||||
* user is not also flashing another module at the same time */
|
||||
App.foreground().startActivity(intent);
|
||||
} else {
|
||||
/* Or else we preset a notification notifying that we are done */
|
||||
PendingIntent pi = PendingIntent.getActivity(this, progress.hashCode(), intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
progress.dlDone(pi);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
progress.dlFail();
|
||||
}
|
||||
removeNotification(progress);
|
||||
}
|
||||
|
||||
private void processZip(InputStream in, OutputStream out)
|
||||
throws IOException {
|
||||
try (ZipInputStream zin = new ZipInputStream(in);
|
||||
ZipOutputStream zout = new ZipOutputStream(out)) {
|
||||
|
||||
// Inject latest module-installer.sh as update-binary
|
||||
zout.putNextEntry(new ZipEntry("META-INF/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/"));
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/update-binary"));
|
||||
try (InputStream update_bin = Networking.get(Const.Url.MODULE_INSTALLER)
|
||||
.execForInputStream().getResult()) {
|
||||
ShellUtils.pump(update_bin, zout);
|
||||
}
|
||||
zout.putNextEntry(new ZipEntry("META-INF/com/google/android/updater-script"));
|
||||
zout.write("#MAGISK\n".getBytes("UTF-8"));
|
||||
|
||||
int off = -1;
|
||||
ZipEntry entry;
|
||||
while ((entry = zin.getNextEntry()) != null) {
|
||||
if (off < 0)
|
||||
off = entry.getName().indexOf('/') + 1;
|
||||
String path = entry.getName().substring(off);
|
||||
if (path.isEmpty())
|
||||
continue;
|
||||
if (path.startsWith("META-INF"))
|
||||
continue;
|
||||
zout.putNextEntry(new ZipEntry(path));
|
||||
if (!entry.isDirectory())
|
||||
ShellUtils.pump(zin, zout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,142 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.chooser
|
||||
import com.topjohnwu.magisk.extensions.provide
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.*
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.Flash.Secondary
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.ui.flash.FlashActivity
|
||||
import com.topjohnwu.magisk.utils.APKInstall
|
||||
import java.io.File
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
/* More of a facade for [RemoteFileService], but whatever... */
|
||||
@SuppressLint("Registered")
|
||||
open class DownloadService : RemoteFileService() {
|
||||
|
||||
private val context get() = this
|
||||
private val File.type
|
||||
get() = MimeTypeMap.getSingleton()
|
||||
.getMimeTypeFromExtension(extension)
|
||||
?: "resource/folder"
|
||||
|
||||
override fun onFinished(subject: DownloadSubject, id: Int) = when (subject) {
|
||||
is Magisk -> onFinishedInternal(subject, id)
|
||||
is Module -> onFinishedInternal(subject, id)
|
||||
is Manager -> onFinishedInternal(subject, id)
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Magisk,
|
||||
id: Int
|
||||
) = when (val conf = subject.configuration) {
|
||||
Uninstall -> FlashActivity.uninstall(this, subject.file, id)
|
||||
is Patch -> FlashActivity.patch(this, subject.file, conf.fileUri, id)
|
||||
is Flash -> FlashActivity.flash(this, subject.file, conf is Secondary, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Module,
|
||||
id: Int
|
||||
) = when (subject.configuration) {
|
||||
is Flash -> FlashActivity.install(this, subject.file, id)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
private fun onFinishedInternal(
|
||||
subject: Manager,
|
||||
id: Int
|
||||
) {
|
||||
remove(id)
|
||||
when (subject.configuration) {
|
||||
is APK.Upgrade -> APKInstall.install(this, subject.file)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
= when (subject) {
|
||||
is Magisk -> addActionsInternal(subject)
|
||||
is Module -> addActionsInternal(subject)
|
||||
is Manager -> addActionsInternal(subject)
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Magisk)
|
||||
= when (val conf = subject.configuration) {
|
||||
Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!))
|
||||
.addAction(0, R.string.download_open_self, fileIntent(subject.file))
|
||||
Uninstall -> setContentIntent(FlashActivity.uninstallIntent(context, subject.file))
|
||||
is Flash -> setContentIntent(FlashActivity.flashIntent(context, subject.file, conf is Secondary))
|
||||
is Patch -> setContentIntent(FlashActivity.patchIntent(context, subject.file, conf.fileUri))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Module)
|
||||
= when (subject.configuration) {
|
||||
Download -> addAction(0, R.string.download_open_parent, fileIntent(subject.file.parentFile!!))
|
||||
.addAction(0, R.string.download_open_self, fileIntent(subject.file))
|
||||
is Flash -> setContentIntent(FlashActivity.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
private fun NotificationCompat.Builder.addActionsInternal(subject: Manager)
|
||||
= when (subject.configuration) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file))
|
||||
else -> this
|
||||
}
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.setContentIntent(intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { setContentIntent(it) }
|
||||
|
||||
@Suppress("ReplaceSingleLineLet")
|
||||
private fun NotificationCompat.Builder.addAction(icon: Int, title: Int, intent: Intent) =
|
||||
PendingIntent.getActivity(context, nextInt(), intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
.let { addAction(icon, getString(title), it) }
|
||||
|
||||
// ---
|
||||
|
||||
private fun fileIntent(file: File): Intent {
|
||||
return Intent(Intent.ACTION_VIEW)
|
||||
.setDataAndType(file.provide(this), file.type)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
.chooser()
|
||||
}
|
||||
|
||||
class Builder {
|
||||
lateinit var subject: DownloadSubject
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
inline operator fun invoke(context: Context, argBuilder: Builder.() -> Unit) {
|
||||
val app = context.applicationContext
|
||||
val builder = Builder().apply(argBuilder)
|
||||
val intent = Intent(app, ClassMap[DownloadService::class.java])
|
||||
.putExtra(ARG_URL, builder.subject)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
app.startForegroundService(intent)
|
||||
} else {
|
||||
app.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.content.ComponentName
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Restore
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration.APK.Upgrade
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.ui.SplashActivity
|
||||
import com.topjohnwu.magisk.utils.DynamicClassLoader
|
||||
import com.topjohnwu.magisk.utils.PatchAPK
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
|
||||
private fun RemoteFileService.patchPackage(apk: File, id: Int) {
|
||||
if (packageName != BuildConfig.APPLICATION_ID) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.hide_manager_title))
|
||||
.setContentText("")
|
||||
}
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
try {
|
||||
// Try using the new APK to patch itself
|
||||
val loader = DynamicClassLoader(apk)
|
||||
loader.loadClass("a.a")
|
||||
.getMethod("patchAPK", String::class.java, String::class.java, String::class.java)
|
||||
.invoke(null, apk.path, patched.path, packageName)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
// Fallback to use the current implementation
|
||||
PatchAPK.patch(apk.path, patched.path, packageName)
|
||||
}
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RemoteFileService.restore(apk: File, id: Int) {
|
||||
update(id) { notification ->
|
||||
notification.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
}
|
||||
Config.export()
|
||||
// Make it world readable
|
||||
apk.setReadable(true, false)
|
||||
if (Shell.su("pm install $apk").exec().isSuccess) {
|
||||
val component = ComponentName(BuildConfig.APPLICATION_ID,
|
||||
ClassMap.get<Class<*>>(SplashActivity::class.java).name)
|
||||
RootUtils.rmAndLaunch(packageName, component)
|
||||
}
|
||||
}
|
||||
|
||||
fun RemoteFileService.handleAPK(subject: DownloadSubject.Manager)
|
||||
= when (subject.configuration) {
|
||||
is Upgrade -> patchPackage(subject.file, subject.hashCode())
|
||||
is Restore -> restore(subject.file, subject.hashCode())
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
fun InputStream.toModule(file: File, installer: InputStream) {
|
||||
|
||||
val input = ZipInputStream(buffered())
|
||||
val output = ZipOutputStream(file.outputStream().buffered())
|
||||
|
||||
withStreams(input, output) { zin, zout ->
|
||||
zout.putNextEntry(ZipEntry("META-INF/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/"))
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/update-binary"))
|
||||
installer.copyTo(zout)
|
||||
|
||||
zout.putNextEntry(ZipEntry("META-INF/com/google/android/updater-script"))
|
||||
zout.write("#MAGISK\n".toByteArray(charset("UTF-8")))
|
||||
|
||||
var off = -1
|
||||
var entry: ZipEntry? = zin.nextEntry
|
||||
while (entry != null) {
|
||||
if (off < 0) {
|
||||
off = entry.name.indexOf('/') + 1
|
||||
}
|
||||
|
||||
val path = entry.name.substring(off)
|
||||
if (path.isNotEmpty() && !path.startsWith("META-INF")) {
|
||||
zout.putNextEntry(ZipEntry(path))
|
||||
if (!entry.isDirectory) {
|
||||
zin.copyTo(zout)
|
||||
}
|
||||
}
|
||||
|
||||
entry = zin.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import java.util.*
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
abstract class NotificationService : Service() {
|
||||
|
||||
abstract val defaultNotification: NotificationCompat.Builder
|
||||
|
||||
private val manager by lazy { NotificationManagerCompat.from(this) }
|
||||
private val hasNotifications get() = notifications.isNotEmpty()
|
||||
|
||||
private val notifications =
|
||||
Collections.synchronizedMap(mutableMapOf<Int, NotificationCompat.Builder>())
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
notifications.forEach { cancel(it.key) }
|
||||
notifications.clear()
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
fun update(
|
||||
id: Int,
|
||||
body: (NotificationCompat.Builder) -> Unit = {}
|
||||
) {
|
||||
val notification = notifications.getOrPut(id) { defaultNotification }
|
||||
|
||||
notify(id, notification.also(body).build())
|
||||
|
||||
if (notifications.size == 1) {
|
||||
updateForeground()
|
||||
}
|
||||
}
|
||||
|
||||
protected fun finishNotify(
|
||||
id: Int,
|
||||
editBody: (NotificationCompat.Builder) -> NotificationCompat.Builder? = { null }
|
||||
) : Int {
|
||||
val currentNotification = remove(id)?.run(editBody)
|
||||
|
||||
var newId = -1
|
||||
currentNotification?.let {
|
||||
newId = nextInt(Int.MAX_VALUE)
|
||||
notify(newId, it.build())
|
||||
}
|
||||
|
||||
if (!hasNotifications) {
|
||||
stopSelf()
|
||||
}
|
||||
return newId
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun notify(id: Int, notification: Notification) {
|
||||
manager.notify(id, notification)
|
||||
}
|
||||
|
||||
private fun cancel(id: Int) {
|
||||
manager.cancel(id)
|
||||
}
|
||||
|
||||
protected fun remove(id: Int) = notifications.remove(id).also {
|
||||
cancel(id)
|
||||
updateForeground()
|
||||
}
|
||||
|
||||
private fun updateForeground() {
|
||||
if (hasNotifications)
|
||||
startForeground(notifications.keys.first(), notifications.values.first().build())
|
||||
else
|
||||
stopForeground(true)
|
||||
}
|
||||
|
||||
// --
|
||||
|
||||
override fun onBind(p0: Intent?): IBinder? = null
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
package com.topjohnwu.magisk.model.download
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.di.NullActivity
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.writeTo
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject.*
|
||||
import com.topjohnwu.magisk.utils.ProgressInputStream
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.ShellUtils
|
||||
import io.reactivex.Completable
|
||||
import okhttp3.ResponseBody
|
||||
import org.koin.android.ext.android.inject
|
||||
import timber.log.Timber
|
||||
import java.io.InputStream
|
||||
|
||||
abstract class RemoteFileService : NotificationService() {
|
||||
|
||||
private val service: GithubRawServices by inject()
|
||||
|
||||
override val defaultNotification: NotificationCompat.Builder
|
||||
get() = Notifications.progress(this, "")
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
intent?.getParcelableExtra<DownloadSubject>(ARG_URL)?.let { start(it) }
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
private fun start(subject: DownloadSubject) = checkExisting(subject)
|
||||
.onErrorResumeNext { download(subject) }
|
||||
.doOnSubscribe { update(subject.hashCode()) { it.setContentTitle(subject.title) } }
|
||||
.subscribeK(onError = {
|
||||
Timber.e(it)
|
||||
finishNotify(subject.hashCode()) { notification ->
|
||||
notification.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
}
|
||||
}) {
|
||||
val newId = finishNotify(subject)
|
||||
if (get<Activity>() !is NullActivity) {
|
||||
onFinished(subject, newId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkExisting(subject: DownloadSubject) = Completable.fromAction {
|
||||
check(subject is Magisk) { "Download cache is disabled" }
|
||||
|
||||
subject.file.also {
|
||||
check(it.exists() && ShellUtils.checkSum("MD5", it, subject.magisk.md5)) {
|
||||
"The given file does not match checksum"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun download(subject: DownloadSubject) = service.fetchFile(subject.url)
|
||||
.map { it.toStream(subject.hashCode()) }
|
||||
.flatMapCompletable { stream ->
|
||||
when (subject) {
|
||||
is Module -> service.fetchInstaller()
|
||||
.doOnSuccess { stream.toModule(subject.file, it.byteStream()) }
|
||||
.ignoreElement()
|
||||
else -> Completable.fromAction { stream.writeTo(subject.file) }
|
||||
}
|
||||
}.doOnComplete {
|
||||
if (subject is Manager)
|
||||
handleAPK(subject)
|
||||
}
|
||||
|
||||
private fun ResponseBody.toStream(id: Int): InputStream {
|
||||
val maxRaw = contentLength()
|
||||
val max = maxRaw / 1_000_000f
|
||||
|
||||
return ProgressInputStream(byteStream()) {
|
||||
val progress = it / 1_000_000f
|
||||
update(id) { notification ->
|
||||
if (maxRaw > 0) {
|
||||
notification
|
||||
.setProgress(maxRaw.toInt(), it.toInt(), false)
|
||||
.setContentText("%.2f / %.2f MB".format(progress, max))
|
||||
} else {
|
||||
notification.setContentText("%.2f MB / ??".format(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun finishNotify(subject: DownloadSubject) = finishNotify(subject.hashCode()) {
|
||||
it.addActions(subject)
|
||||
.setContentText(getString(R.string.download_complete))
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setProgress(0, 0, false)
|
||||
.setOngoing(false)
|
||||
.setAutoCancel(true)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
|
||||
@Throws(Throwable::class)
|
||||
protected abstract fun onFinished(subject: DownloadSubject, id: Int)
|
||||
|
||||
protected abstract fun NotificationCompat.Builder.addActions(subject: DownloadSubject)
|
||||
: NotificationCompat.Builder
|
||||
|
||||
companion object {
|
||||
const val ARG_URL = "arg_url"
|
||||
}
|
||||
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class BaseModule implements Comparable<BaseModule>, Parcelable {
|
||||
|
||||
private String mId, mName, mVersion, mAuthor, mDescription;
|
||||
private int mVersionCode = -1;
|
||||
|
||||
protected BaseModule() {
|
||||
mId = mName = mVersion = mAuthor = mDescription = "";
|
||||
}
|
||||
|
||||
protected BaseModule(Cursor c) {
|
||||
mId = nonNull(c.getString(c.getColumnIndex("id")));
|
||||
mName = nonNull(c.getString(c.getColumnIndex("name")));
|
||||
mVersion = nonNull(c.getString(c.getColumnIndex("version")));
|
||||
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
|
||||
mAuthor = nonNull(c.getString(c.getColumnIndex("author")));
|
||||
mDescription = nonNull(c.getString(c.getColumnIndex("description")));
|
||||
}
|
||||
|
||||
protected BaseModule(Parcel p) {
|
||||
mId = p.readString();
|
||||
mName = p.readString();
|
||||
mVersion = p.readString();
|
||||
mAuthor = p.readString();
|
||||
mDescription = p.readString();
|
||||
mVersionCode = p.readInt();
|
||||
}
|
||||
|
||||
protected BaseModule(MagiskModule m) {
|
||||
mId = m.getId();
|
||||
mName = m.getName();
|
||||
mVersion = m.getVersion();
|
||||
mAuthor = m.getAuthor();
|
||||
mDescription = m.getDescription();
|
||||
mVersionCode = Integer.parseInt(m.getVersionCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull BaseModule module) {
|
||||
return getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mId);
|
||||
dest.writeString(mName);
|
||||
dest.writeString(mVersion);
|
||||
dest.writeString(mAuthor);
|
||||
dest.writeString(mDescription);
|
||||
dest.writeInt(mVersionCode);
|
||||
}
|
||||
|
||||
private String nonNull(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("id", mId);
|
||||
values.put("name", mName);
|
||||
values.put("version", mVersion);
|
||||
values.put("versionCode", mVersionCode);
|
||||
values.put("author", mAuthor);
|
||||
values.put("description", mDescription);
|
||||
return values;
|
||||
}
|
||||
|
||||
protected void parseProps(List<String> props) {
|
||||
parseProps(props.toArray(new String[0]));
|
||||
}
|
||||
|
||||
protected void parseProps(String[] props) throws NumberFormatException {
|
||||
for (String line : props) {
|
||||
String[] prop = line.split("=", 2);
|
||||
if (prop.length != 2)
|
||||
continue;
|
||||
|
||||
String key = prop[0].trim();
|
||||
String value = prop[1].trim();
|
||||
if (key.isEmpty() || key.charAt(0) == '#')
|
||||
continue;
|
||||
|
||||
switch (key) {
|
||||
case "id":
|
||||
mId = value;
|
||||
break;
|
||||
case "name":
|
||||
mName = value;
|
||||
break;
|
||||
case "version":
|
||||
mVersion = value;
|
||||
break;
|
||||
case "versionCode":
|
||||
mVersionCode = Integer.parseInt(value);
|
||||
break;
|
||||
case "author":
|
||||
mAuthor = value;
|
||||
break;
|
||||
case "description":
|
||||
mDescription = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
mName = name;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return mVersion;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return mAuthor;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return mId;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
mId = id;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public int getVersionCode() {
|
||||
return mVersionCode;
|
||||
}
|
||||
|
||||
}
|
@@ -2,8 +2,8 @@ package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.utils.packageInfo
|
||||
import com.topjohnwu.magisk.utils.processes
|
||||
import com.topjohnwu.magisk.extensions.packageInfo
|
||||
import com.topjohnwu.magisk.extensions.processes
|
||||
|
||||
class HideAppInfo(
|
||||
val info: ApplicationInfo,
|
||||
|
@@ -1,11 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskApp(
|
||||
val version: String,
|
||||
val versionCode: String,
|
||||
val link: String,
|
||||
val note: String
|
||||
)
|
@@ -1,10 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskConfig(
|
||||
val app: MagiskApp,
|
||||
val uninstaller: MagiskLink,
|
||||
val magisk: MagiskFlashable
|
||||
)
|
@@ -1,13 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskFlashable(
|
||||
val version: String,
|
||||
val versionCode: String,
|
||||
val link: String,
|
||||
val note: String,
|
||||
@Json(name = "md5") val hash: String
|
||||
)
|
@@ -1,8 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskLink(
|
||||
val link: String
|
||||
)
|
@@ -1,8 +1,8 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import com.topjohnwu.magisk.extensions.timeFormatTime
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.ALLOW
|
||||
import com.topjohnwu.magisk.utils.timeFormatTime
|
||||
import com.topjohnwu.magisk.utils.toTime
|
||||
import java.util.*
|
||||
|
||||
data class MagiskLog(
|
||||
@@ -38,7 +38,6 @@ fun Map<String, String>.toLog(): MagiskLog {
|
||||
|
||||
fun Long.toDate() = Date(this)
|
||||
|
||||
|
||||
fun MagiskLog.toMap() = mapOf(
|
||||
"from_uid" to fromUid,
|
||||
"to_uid" to toUid,
|
||||
@@ -47,8 +46,8 @@ fun MagiskLog.toMap() = mapOf(
|
||||
"app_name" to appName,
|
||||
"command" to command,
|
||||
"action" to action,
|
||||
"time" to date
|
||||
).mapValues { it.toString() }
|
||||
"time" to date.time
|
||||
)
|
||||
|
||||
fun MagiskPolicy.toLog(
|
||||
toUid: Int,
|
||||
|
@@ -1,86 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.NonNull
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Constants
|
||||
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 = "${Constants.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
|
||||
)
|
@@ -2,6 +2,7 @@ package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import com.topjohnwu.magisk.extensions.getLabel
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy.Companion.INTERACTIVE
|
||||
|
||||
|
||||
@@ -24,42 +25,14 @@ data class MagiskPolicy(
|
||||
|
||||
}
|
||||
|
||||
/*@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun ContentValues.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
val uid = getAsInteger("uid")
|
||||
val packageName = getAsString("package_name")
|
||||
val info = pm.getApplicationInfo(packageName, 0)
|
||||
if (info.uid != uid)
|
||||
throw PackageManager.NameNotFoundException()
|
||||
return MagiskPolicy(
|
||||
uid = uid,
|
||||
packageName = packageName,
|
||||
policy = getAsInteger("policy"),
|
||||
until = getAsInteger("until").toLong(),
|
||||
logging = getAsInteger("logging") != 0,
|
||||
notification = getAsInteger("notification") != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
)
|
||||
}
|
||||
fun MagiskPolicy.toContentValues() = ContentValues().apply {
|
||||
put("uid", uid)
|
||||
put("uid", uid)
|
||||
put("package_name", packageName)
|
||||
put("policy", policy)
|
||||
put("until", until)
|
||||
put("logging", if (logging) 1 else 0)
|
||||
put("notification", if (notification) 1 else 0)
|
||||
}*/
|
||||
|
||||
fun MagiskPolicy.toMap() = mapOf(
|
||||
"uid" to uid,
|
||||
"package_name" to packageName,
|
||||
"policy" to policy,
|
||||
"until" to until,
|
||||
"logging" to if (logging) 1 else 0,
|
||||
"notification" to if (notification) 1 else 0
|
||||
).mapValues { it.value.toString() }
|
||||
"logging" to logging,
|
||||
"notification" to notification
|
||||
)
|
||||
|
||||
@Throws(PackageManager.NameNotFoundException::class)
|
||||
fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
@@ -78,7 +51,7 @@ fun Map<String, String>.toPolicy(pm: PackageManager): MagiskPolicy {
|
||||
logging = get("logging")?.toIntOrNull() != 0,
|
||||
notification = get("notification")?.toIntOrNull() != 0,
|
||||
applicationInfo = info,
|
||||
appName = info.loadLabel(pm).toString()
|
||||
appName = info.getLabel(pm)
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,83 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
|
||||
public class OldModule extends BaseModule {
|
||||
|
||||
public static final Parcelable.Creator<OldModule> CREATOR = new Creator<OldModule>() {
|
||||
/* It won't be used at any place */
|
||||
@Override
|
||||
public OldModule createFromParcel(Parcel source) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OldModule[] newArray(int size) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
private final SuFile mRemoveFile;
|
||||
private final SuFile mDisableFile;
|
||||
private final SuFile mUpdateFile;
|
||||
private final boolean mUpdated;
|
||||
private boolean mEnable;
|
||||
private boolean mRemove;
|
||||
|
||||
public OldModule(String path) {
|
||||
|
||||
try {
|
||||
parseProps(Shell.su("dos2unix < " + path + "/module.prop").exec().getOut());
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
|
||||
mRemoveFile = new SuFile(path, "remove");
|
||||
mDisableFile = new SuFile(path, "disable");
|
||||
mUpdateFile = new SuFile(path, "update");
|
||||
|
||||
if (getId().isEmpty()) {
|
||||
int sep = path.lastIndexOf('/');
|
||||
setId(path.substring(sep + 1));
|
||||
}
|
||||
|
||||
if (getName().isEmpty()) {
|
||||
setName(getId());
|
||||
}
|
||||
|
||||
mEnable = !mDisableFile.exists();
|
||||
mRemove = mRemoveFile.exists();
|
||||
mUpdated = mUpdateFile.exists();
|
||||
}
|
||||
|
||||
public void createDisableFile() {
|
||||
mEnable = !mDisableFile.createNewFile();
|
||||
}
|
||||
|
||||
public void removeDisableFile() {
|
||||
mEnable = mDisableFile.delete();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnable;
|
||||
}
|
||||
|
||||
public void createRemoveFile() {
|
||||
mRemove = mRemoveFile.createNewFile();
|
||||
}
|
||||
|
||||
public void deleteRemoveFile() {
|
||||
mRemove = !mRemoveFile.delete();
|
||||
}
|
||||
|
||||
public boolean willBeRemoved() {
|
||||
return mRemove;
|
||||
}
|
||||
|
||||
public boolean isUpdated() {
|
||||
return mUpdated;
|
||||
}
|
||||
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
public class Policy implements Comparable<Policy>{
|
||||
public static final int INTERACTIVE = 0;
|
||||
public static final int DENY = 1;
|
||||
public static final int ALLOW = 2;
|
||||
|
||||
public int uid, policy = INTERACTIVE;
|
||||
public long until;
|
||||
public boolean logging = true, notification = true;
|
||||
public String packageName, appName;
|
||||
public ApplicationInfo info;
|
||||
|
||||
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
String[] pkgs = pm.getPackagesForUid(uid);
|
||||
if (pkgs == null || pkgs.length == 0)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
this.uid = uid;
|
||||
packageName = pkgs[0];
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
appName = Utils.getAppLabel(info, pm);
|
||||
}
|
||||
|
||||
public Policy(ContentValues values, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||
uid = values.getAsInteger("uid");
|
||||
packageName = values.getAsString("package_name");
|
||||
policy = values.getAsInteger("policy");
|
||||
until = values.getAsInteger("until");
|
||||
logging = values.getAsInteger("logging") != 0;
|
||||
notification = values.getAsInteger("notification") != 0;
|
||||
info = pm.getApplicationInfo(packageName, 0);
|
||||
if (info.uid != uid)
|
||||
throw new PackageManager.NameNotFoundException();
|
||||
appName = info.loadLabel(pm).toString();
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("uid", uid);
|
||||
values.put("package_name", packageName);
|
||||
values.put("policy", policy);
|
||||
values.put("until", until);
|
||||
values.put("logging", logging ? 1 : 0);
|
||||
values.put("notification", notification ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NonNull Policy policy) {
|
||||
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
|
||||
}
|
||||
}
|
@@ -1,114 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class Repo extends BaseModule {
|
||||
|
||||
private Date mLastUpdate;
|
||||
|
||||
public Repo(String id) {
|
||||
setId(id);
|
||||
}
|
||||
|
||||
public Repo(Cursor c) {
|
||||
super(c);
|
||||
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
||||
}
|
||||
|
||||
public Repo(Parcel p) {
|
||||
super(p);
|
||||
mLastUpdate = new Date(p.readLong());
|
||||
}
|
||||
|
||||
public Repo(Repository repo) {
|
||||
super(repo);
|
||||
mLastUpdate = new Date(repo.getLastUpdate());
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Repo> CREATOR = new Parcelable.Creator<Repo>() {
|
||||
|
||||
@Override
|
||||
public Repo createFromParcel(Parcel source) {
|
||||
return new Repo(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Repo[] newArray(int size) {
|
||||
return new Repo[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeLong(mLastUpdate.getTime());
|
||||
}
|
||||
|
||||
public void update() throws IllegalRepoException {
|
||||
String[] props = Utils.dlString(getPropUrl()).split("\\n");
|
||||
try {
|
||||
parseProps(props);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalRepoException("Repo [" + getId() + "] parse error: " + e.getMessage());
|
||||
}
|
||||
|
||||
if (getVersionCode() < 0) {
|
||||
throw new IllegalRepoException("Repo [" + getId() + "] does not contain versionCode");
|
||||
}
|
||||
}
|
||||
|
||||
public void update(Date lastUpdate) throws IllegalRepoException {
|
||||
mLastUpdate = lastUpdate;
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = super.getContentValues();
|
||||
values.put("last_update", mLastUpdate.getTime());
|
||||
return values;
|
||||
}
|
||||
|
||||
public String getZipUrl() {
|
||||
return String.format(Const.Url.ZIP_URL, getId());
|
||||
}
|
||||
|
||||
public String getPropUrl() {
|
||||
return getFileUrl("module.prop");
|
||||
}
|
||||
|
||||
public String getDetailUrl() {
|
||||
return getFileUrl("README.md");
|
||||
}
|
||||
|
||||
public String getFileUrl(String file) {
|
||||
return String.format(Const.Url.FILE_URL, getId(), file);
|
||||
}
|
||||
|
||||
public String getLastUpdateString() {
|
||||
return DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM).format(mLastUpdate);
|
||||
}
|
||||
|
||||
public Date getLastUpdate() {
|
||||
return mLastUpdate;
|
||||
}
|
||||
|
||||
public String getDownloadFilename() {
|
||||
return Utils.getLegalFilename(getName() + "-" + getVersion() + ".zip");
|
||||
}
|
||||
|
||||
public class IllegalRepoException extends Exception {
|
||||
IllegalRepoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,56 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.entity;
|
||||
|
||||
import android.content.ContentValues;
|
||||
|
||||
import com.topjohnwu.magisk.utils.LocaleManager;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
public class SuLogEntry {
|
||||
|
||||
public int fromUid, toUid, fromPid;
|
||||
public String packageName, appName, command;
|
||||
public boolean action;
|
||||
public Date date;
|
||||
|
||||
public SuLogEntry(MagiskPolicy policy) {
|
||||
fromUid = policy.getUid();
|
||||
packageName = policy.getPackageName();
|
||||
appName = policy.getAppName();
|
||||
action = policy.getPolicy() == Policy.ALLOW;
|
||||
}
|
||||
|
||||
public SuLogEntry(ContentValues values) {
|
||||
fromUid = values.getAsInteger("from_uid");
|
||||
packageName = values.getAsString("package_name");
|
||||
appName = values.getAsString("app_name");
|
||||
fromPid = values.getAsInteger("from_pid");
|
||||
command = values.getAsString("command");
|
||||
toUid = values.getAsInteger("to_uid");
|
||||
action = values.getAsInteger("action") != 0;
|
||||
date = new Date(values.getAsLong("time"));
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("from_uid", fromUid);
|
||||
values.put("package_name", packageName);
|
||||
values.put("app_name", appName);
|
||||
values.put("from_pid", fromPid);
|
||||
values.put("command", command);
|
||||
values.put("to_uid", toUid);
|
||||
values.put("action", action ? 1 : 0);
|
||||
values.put("time", date.getTime());
|
||||
return values;
|
||||
}
|
||||
|
||||
public String getDateString() {
|
||||
return DateFormat.getDateInstance(DateFormat.MEDIUM, LocaleManager.getLocale()).format(date);
|
||||
}
|
||||
|
||||
public String getTimeString() {
|
||||
return new SimpleDateFormat("h:mm a", LocaleManager.getLocale()).format(date);
|
||||
}
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
package com.topjohnwu.magisk.model.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import se.ansman.kotshi.JsonSerializable
|
||||
|
||||
@JsonSerializable
|
||||
data class UpdateInfo(
|
||||
val app: ManagerJson = ManagerJson(),
|
||||
val uninstaller: UninstallerJson = UninstallerJson(),
|
||||
val magisk: MagiskJson = MagiskJson()
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class UninstallerJson(
|
||||
val link: String = ""
|
||||
)
|
||||
|
||||
@JsonSerializable
|
||||
data class MagiskJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = "",
|
||||
val md5: String = ""
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
@JsonSerializable
|
||||
data class ManagerJson(
|
||||
val version: String = "",
|
||||
val versionCode: Int = -1,
|
||||
val link: String = "",
|
||||
val note: String = ""
|
||||
) : Parcelable
|
@@ -0,0 +1,37 @@
|
||||
package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
sealed class Configuration : Parcelable {
|
||||
|
||||
sealed class Flash : Configuration() {
|
||||
|
||||
@Parcelize
|
||||
object Primary : Flash()
|
||||
|
||||
@Parcelize
|
||||
object Secondary : Flash()
|
||||
|
||||
}
|
||||
|
||||
sealed class APK : Configuration() {
|
||||
|
||||
@Parcelize
|
||||
object Upgrade : APK()
|
||||
|
||||
@Parcelize
|
||||
object Restore : APK()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
object Download : Configuration()
|
||||
|
||||
@Parcelize
|
||||
object Uninstall : Configuration()
|
||||
|
||||
@Parcelize
|
||||
data class Patch(val fileUri: Uri) : Configuration()
|
||||
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
package com.topjohnwu.magisk.model.entity.internal
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.extensions.cachedFile
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.model.entity.MagiskJson
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.io.File
|
||||
|
||||
sealed class DownloadSubject : Parcelable {
|
||||
|
||||
abstract val url: String
|
||||
abstract val file: File
|
||||
open val title: String get() = file.name
|
||||
|
||||
@Parcelize
|
||||
data class Module(
|
||||
val module: Repo,
|
||||
val configuration: Configuration
|
||||
) : DownloadSubject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
File(Config.downloadDirectory, module.downloadFilename)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class Manager(
|
||||
val configuration: Configuration.APK
|
||||
) : DownloadSubject() {
|
||||
|
||||
@IgnoredOnParcel
|
||||
val manager: ManagerJson = Info.remote.app
|
||||
|
||||
override val title: String
|
||||
get() = "MagiskManager-v${manager.version}(${manager.versionCode})"
|
||||
|
||||
override val url: String
|
||||
get() = manager.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("manager.apk")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sealed class Magisk : DownloadSubject() {
|
||||
|
||||
abstract val configuration: Configuration
|
||||
val magisk: MagiskJson = Info.remote.magisk
|
||||
|
||||
@Parcelize
|
||||
protected data class Flash(
|
||||
override val configuration: Configuration
|
||||
) : Magisk() {
|
||||
override val url: String get() = magisk.link
|
||||
override val title: String get() = "Magisk-v${magisk.version}(${magisk.versionCode})"
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("magisk.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Uninstall : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Uninstall
|
||||
override val url: String get() = Info.remote.uninstaller.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
get<Context>().cachedFile("uninstall.zip")
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
protected class Download : Magisk() {
|
||||
override val configuration: Configuration get() = Configuration.Download
|
||||
override val url: String get() = magisk.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
File(Config.downloadDirectory, "Magisk-v${magisk.version}(${magisk.versionCode}).zip")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
operator fun invoke(configuration: Configuration) = when (configuration) {
|
||||
Configuration.Download -> Download()
|
||||
Configuration.Uninstall -> Uninstall()
|
||||
else -> Flash(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
|
||||
class Module(path: String) : BaseModule() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile: SuFile = SuFile(path, "remove")
|
||||
private val disableFile: SuFile = SuFile(path, "disable")
|
||||
private val updateFile: SuFile = SuFile(path, "update")
|
||||
|
||||
val updated: Boolean = updateFile.exists()
|
||||
|
||||
var enable: Boolean = !disableFile.exists()
|
||||
set(enable) {
|
||||
field = if (enable) {
|
||||
disableFile.delete()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean = removeFile.exists()
|
||||
set(remove) {
|
||||
field = if (remove) {
|
||||
removeFile.createNewFile()
|
||||
} else {
|
||||
!removeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id;
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@WorkerThread
|
||||
fun loadModules(): List<Module> {
|
||||
val moduleList = mutableListOf<Module>()
|
||||
val path = SuFile(Const.MAGISK_PATH)
|
||||
val modules =
|
||||
path.listFiles { _, name -> name != "lost+found" && name != ".core" }.orEmpty()
|
||||
for (file in modules) {
|
||||
if (file.isFile) continue
|
||||
val module = Module(Const.MAGISK_PATH + "/" + file.name)
|
||||
moduleList.add(module)
|
||||
}
|
||||
return moduleList
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,71 @@
|
||||
package com.topjohnwu.magisk.model.entity.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.data.repository.StringRepository
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.legalFilename
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repo(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String,
|
||||
override var author: String,
|
||||
override var version: String,
|
||||
override var versionCode: Int,
|
||||
override var description: String,
|
||||
var last_update: Long
|
||||
) : BaseModule(), Parcelable {
|
||||
|
||||
private val stringRepo: StringRepository get() = get()
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
|
||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
val readme get() = stringRepo.getReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update() {
|
||||
val props = runCatching {
|
||||
stringRepo.getMetadata(this).blockingGet()
|
||||
.orEmpty().split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
}.getOrElse {
|
||||
throw IllegalRepoException("Repo [$id] module.prop download error: " + it.message)
|
||||
}
|
||||
|
||||
props.runCatching {
|
||||
parseProps(this)
|
||||
}.onFailure {
|
||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
fun update(lastUpdate: Date) {
|
||||
last_update = lastUpdate.time
|
||||
update()
|
||||
}
|
||||
|
||||
class IllegalRepoException(message: String) : Exception(message)
|
||||
|
||||
companion object {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)!!
|
||||
}
|
||||
}
|
@@ -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
|
||||
}
|
@@ -6,12 +6,12 @@ import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.HideAppInfo
|
||||
import com.topjohnwu.magisk.model.entity.HideTarget
|
||||
import com.topjohnwu.magisk.model.entity.state.IndeterminateState
|
||||
import com.topjohnwu.magisk.model.events.HideProcessEvent
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class HideRvItem(val item: HideAppInfo, targets: List<HideTarget>) :
|
||||
ComparableRvItem<HideRvItem>() {
|
||||
|
@@ -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) {}
|
||||
|
||||
}
|
@@ -4,11 +4,11 @@ import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.util.DiffObservableList
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.timeFormatMedium
|
||||
import com.topjohnwu.magisk.extensions.toTime
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskLog
|
||||
import com.topjohnwu.magisk.model.entity.WrappedMagiskLog
|
||||
import com.topjohnwu.magisk.utils.timeFormatMedium
|
||||
import com.topjohnwu.magisk.utils.toTime
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class LogRvItem : ComparableRvItem<LogRvItem>() {
|
||||
override val layoutRes: Int = R.layout.item_page_log
|
||||
|
@@ -6,44 +6,54 @@ import com.skoumal.teanity.databinding.ComparableRvItem
|
||||
import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.model.entity.OldModule
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import com.topjohnwu.magisk.model.entity.Repository
|
||||
import com.topjohnwu.magisk.utils.get
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
import com.topjohnwu.magisk.extensions.get
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.module.Module
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
|
||||
class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
|
||||
class ModuleRvItem(val item: Module) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_module
|
||||
|
||||
val lastActionNotice = KObservableField("")
|
||||
val isChecked = KObservableField(item.isEnabled)
|
||||
val isDeletable = KObservableField(item.willBeRemoved())
|
||||
val isChecked = KObservableField(item.enable)
|
||||
val isDeletable = KObservableField(item.remove)
|
||||
|
||||
init {
|
||||
isChecked.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> item.removeDisableFile().notice(R.string.disable_file_removed)
|
||||
false -> item.createDisableFile().notice(R.string.disable_file_created)
|
||||
true -> {
|
||||
item.enable = true
|
||||
notice(R.string.disable_file_removed)
|
||||
}
|
||||
false -> {
|
||||
item.enable = false
|
||||
notice(R.string.disable_file_created)
|
||||
}
|
||||
}
|
||||
}
|
||||
isDeletable.addOnPropertyChangedCallback {
|
||||
when (it) {
|
||||
true -> item.createRemoveFile().notice(R.string.remove_file_created)
|
||||
false -> item.deleteRemoveFile().notice(R.string.remove_file_deleted)
|
||||
true -> {
|
||||
item.remove = true
|
||||
notice(R.string.remove_file_created)
|
||||
}
|
||||
false -> {
|
||||
item.remove = false
|
||||
notice(R.string.remove_file_deleted)
|
||||
}
|
||||
}
|
||||
}
|
||||
when {
|
||||
item.isUpdated -> notice(R.string.update_file_created)
|
||||
item.willBeRemoved() -> notice(R.string.remove_file_created)
|
||||
item.updated -> notice(R.string.update_file_created)
|
||||
item.remove -> notice(R.string.remove_file_created)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggle() = isChecked.toggle()
|
||||
fun toggleDelete() = isDeletable.toggle()
|
||||
|
||||
@Suppress("unused")
|
||||
private fun Any.notice(@StringRes info: Int) {
|
||||
private fun notice(@StringRes info: Int) {
|
||||
lastActionNotice.value = get<Resources>().getString(info)
|
||||
}
|
||||
|
||||
@@ -57,15 +67,9 @@ class ModuleRvItem(val item: OldModule) : ComparableRvItem<ModuleRvItem>() {
|
||||
|
||||
class RepoRvItem(val item: Repo) : ComparableRvItem<RepoRvItem>() {
|
||||
|
||||
constructor(repo: Repository) : this(Repo(repo))
|
||||
|
||||
override val layoutRes: Int = R.layout.item_repo
|
||||
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item.version == other.item.version
|
||||
&& item.lastUpdate == other.item.lastUpdate
|
||||
&& item.versionCode == other.item.versionCode
|
||||
&& item.description == other.item.description
|
||||
&& item.detailUrl == other.item.detailUrl
|
||||
override fun contentSameAs(other: RepoRvItem): Boolean = item == other.item
|
||||
|
||||
override fun itemSameAs(other: RepoRvItem): Boolean = item.id == other.item.id
|
||||
}
|
@@ -6,19 +6,18 @@ import com.skoumal.teanity.extensions.addOnPropertyChangedCallback
|
||||
import com.skoumal.teanity.rxbus.RxBus
|
||||
import com.skoumal.teanity.util.KObservableField
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.toggle
|
||||
import com.topjohnwu.magisk.model.entity.MagiskPolicy
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.events.PolicyEnableEvent
|
||||
import com.topjohnwu.magisk.model.events.PolicyUpdateEvent
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.toggle
|
||||
|
||||
class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvItem<PolicyRvItem>() {
|
||||
|
||||
override val layoutRes: Int = R.layout.item_policy
|
||||
|
||||
val isExpanded = KObservableField(false)
|
||||
val isEnabled = KObservableField(item.policy == Policy.ALLOW)
|
||||
val isEnabled = KObservableField(item.policy == MagiskPolicy.ALLOW)
|
||||
val shouldNotify = KObservableField(item.notification)
|
||||
val shouldLog = KObservableField(item.logging)
|
||||
|
||||
@@ -28,7 +27,7 @@ class PolicyRvItem(val item: MagiskPolicy, val icon: Drawable) : ComparableRvIte
|
||||
|
||||
private val currentStateItem
|
||||
get() = item.copy(
|
||||
policy = if (isEnabled.value) Policy.ALLOW else Policy.DENY,
|
||||
policy = if (isEnabled.value) MagiskPolicy.ALLOW else MagiskPolicy.DENY,
|
||||
notification = shouldNotify.value,
|
||||
logging = shouldLog.value
|
||||
)
|
||||
|
@@ -2,8 +2,7 @@ package com.topjohnwu.magisk.model.events
|
||||
|
||||
import android.app.Activity
|
||||
import com.skoumal.teanity.viewevents.ViewEvent
|
||||
import com.topjohnwu.magisk.model.entity.Policy
|
||||
import com.topjohnwu.magisk.model.entity.Repo
|
||||
import com.topjohnwu.magisk.model.entity.module.Repo
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
|
||||
|
||||
@@ -36,5 +35,4 @@ class PermissionEvent(
|
||||
|
||||
class BackPressEvent : ViewEvent()
|
||||
|
||||
class SuDialogEvent(val policy: Policy) : ViewEvent()
|
||||
class DieEvent : ViewEvent()
|
@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.model.flash
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.os.postDelayed
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.tasks.FlashZip
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler
|
||||
|
||||
|
@@ -5,10 +5,11 @@ import com.topjohnwu.magisk.tasks.MagiskInstaller
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
sealed class Patching(
|
||||
file: Uri,
|
||||
private val console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
private val resultListener: FlashResultListener
|
||||
) : MagiskInstaller(console, logs) {
|
||||
) : MagiskInstaller(file, console, logs) {
|
||||
|
||||
override fun onResult(success: Boolean) {
|
||||
if (success) {
|
||||
@@ -21,29 +22,32 @@ sealed class Patching(
|
||||
}
|
||||
|
||||
class File(
|
||||
file: Uri,
|
||||
private val uri: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Patching(console, logs, resultListener) {
|
||||
) : Patching(file, console, logs, resultListener) {
|
||||
override fun operations() =
|
||||
extractZip() && handleFile(uri) && patchBoot() && storeBoot()
|
||||
}
|
||||
|
||||
class SecondSlot(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Patching(console, logs, resultListener) {
|
||||
) : Patching(file, console, logs, resultListener) {
|
||||
override fun operations() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
}
|
||||
|
||||
class Direct(
|
||||
file: Uri,
|
||||
console: MutableList<String>,
|
||||
logs: MutableList<String>,
|
||||
resultListener: FlashResultListener
|
||||
) : Patching(console, logs, resultListener) {
|
||||
) : Patching(file, console, logs, resultListener) {
|
||||
override fun operations() =
|
||||
findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import com.topjohnwu.magisk.extensions.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -16,7 +16,7 @@ class BooleanProperty(
|
||||
property: KProperty<*>
|
||||
): Boolean {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import com.topjohnwu.magisk.extensions.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -16,7 +16,7 @@ class FloatProperty(
|
||||
property: KProperty<*>
|
||||
): Float {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import com.topjohnwu.magisk.extensions.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -16,7 +16,7 @@ class IntProperty(
|
||||
property: KProperty<*>
|
||||
): Int {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import com.topjohnwu.magisk.extensions.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -16,7 +16,7 @@ class LongProperty(
|
||||
property: KProperty<*>
|
||||
): Long {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
|
@@ -1,51 +1,69 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
abstract class PreferenceModel(
|
||||
private val commitPrefs: Boolean = false
|
||||
) {
|
||||
interface PreferenceModel {
|
||||
|
||||
protected abstract val fileName: String
|
||||
protected abstract val context: Context
|
||||
val context: Context
|
||||
|
||||
internal val prefs get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
val commitPrefs: Boolean
|
||||
get() = false
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
protected fun preference(
|
||||
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
|
||||
): ReadWriteProperty<PreferenceModel, Boolean> = BooleanProperty(name, default, commit)
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Float> = FloatProperty(name, default, commit)
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Int> = IntProperty(name, default, commit)
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Long> = LongProperty(name, default, commit)
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, String> = StringProperty(name, default, commit)
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
protected fun preference(
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = commitPrefs
|
||||
): ReadWriteProperty<PreferenceModel, Set<String>> = StringSetProperty(name, default, commit)
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import com.topjohnwu.magisk.extensions.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -16,7 +16,7 @@ class StringProperty(
|
||||
property: KProperty<*>
|
||||
): String {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.preference
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.topjohnwu.magisk.utils.trimEmptyToNull
|
||||
import com.topjohnwu.magisk.extensions.trimEmptyToNull
|
||||
import kotlin.properties.ReadWriteProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@@ -16,7 +16,7 @@ class StringSetProperty(
|
||||
property: KProperty<*>
|
||||
): Set<String> {
|
||||
val prefName = name.trimEmptyToNull() ?: property.name
|
||||
return runCatching { thisRef.prefs.get(prefName, default) }.getOrNull() ?: default
|
||||
return thisRef.prefs.get(prefName, default)
|
||||
}
|
||||
|
||||
override operator fun setValue(
|
||||
|
@@ -6,21 +6,24 @@ import android.content.Intent
|
||||
import com.topjohnwu.magisk.ClassMap
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.database.PolicyDao
|
||||
import com.topjohnwu.magisk.data.database.base.su
|
||||
import com.topjohnwu.magisk.data.repository.AppRepository
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.reboot
|
||||
import com.topjohnwu.magisk.model.download.DownloadService
|
||||
import com.topjohnwu.magisk.model.entity.ManagerJson
|
||||
import com.topjohnwu.magisk.model.entity.internal.Configuration
|
||||
import com.topjohnwu.magisk.model.entity.internal.DownloadSubject
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.magisk.utils.DownloadApp
|
||||
import com.topjohnwu.magisk.utils.RootUtils
|
||||
import com.topjohnwu.magisk.utils.SuLogger
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.get
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
open class GeneralReceiver : BroadcastReceiver() {
|
||||
|
||||
private val appRepo: AppRepository by inject()
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
||||
companion object {
|
||||
const val REQUEST = "request"
|
||||
@@ -30,21 +33,19 @@ open class GeneralReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
private fun getPkg(intent: Intent): String {
|
||||
return intent.data?.encodedSchemeSpecificPart ?: ""
|
||||
return intent.data?.encodedSchemeSpecificPart.orEmpty()
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent?) {
|
||||
if (intent == null)
|
||||
return
|
||||
var action: String? = intent.action ?: return
|
||||
when (action) {
|
||||
intent ?: return
|
||||
when (intent.action ?: return) {
|
||||
Intent.ACTION_REBOOT, Intent.ACTION_BOOT_COMPLETED -> {
|
||||
action = intent.getStringExtra("action")
|
||||
val action = intent.getStringExtra("action")
|
||||
if (action == null) {
|
||||
// Actual boot completed event
|
||||
Shell.su("mm_patch_dtbo").submit { result ->
|
||||
if (result.isSuccess)
|
||||
Notifications.dtboPatched()
|
||||
Shell.su("mm_patch_dtbo").submit {
|
||||
if (it.isSuccess)
|
||||
Notifications.dtboPatched(context)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -64,20 +65,23 @@ open class GeneralReceiver : BroadcastReceiver() {
|
||||
}
|
||||
Intent.ACTION_PACKAGE_REPLACED ->
|
||||
// This will only work pre-O
|
||||
if (Config.get<Boolean>(Config.Key.SU_REAUTH)!!) {
|
||||
appRepo.delete(getPkg(intent)).blockingGet()
|
||||
}
|
||||
if (Config.suReAuth)
|
||||
policyDB.delete(getPkg(intent)).blockingGet()
|
||||
Intent.ACTION_PACKAGE_FULLY_REMOVED -> {
|
||||
val pkg = getPkg(intent)
|
||||
appRepo.delete(pkg).blockingGet()
|
||||
policyDB.delete(pkg).blockingGet()
|
||||
"magiskhide --rm $pkg".su().blockingGet()
|
||||
}
|
||||
Intent.ACTION_LOCALE_CHANGED -> Shortcuts.setup(context)
|
||||
Const.Key.BROADCAST_MANAGER_UPDATE -> {
|
||||
Config.managerLink = intent.getStringExtra(Const.Key.INTENT_SET_LINK)
|
||||
DownloadApp.upgrade(intent.getStringExtra(Const.Key.INTENT_SET_NAME))
|
||||
intent.getParcelableExtra<ManagerJson>(Const.Key.INTENT_SET_APP)?.let {
|
||||
Info.remote = Info.remote.copy(app = it)
|
||||
}
|
||||
DownloadService(context) {
|
||||
subject = DownloadSubject.Manager(Configuration.APK.Upgrade)
|
||||
}
|
||||
}
|
||||
Const.Key.BROADCAST_REBOOT -> RootUtils.reboot()
|
||||
Const.Key.BROADCAST_REBOOT -> reboot()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,30 +2,29 @@ package com.topjohnwu.magisk.model.update
|
||||
|
||||
import androidx.work.ListenableWorker
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.Config
|
||||
import com.topjohnwu.magisk.Info
|
||||
import com.topjohnwu.magisk.data.repository.MagiskRepository
|
||||
import com.topjohnwu.magisk.model.entity.MagiskConfig
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.model.worker.DelegateWorker
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.Shell
|
||||
|
||||
class UpdateCheckService : DelegateWorker() {
|
||||
|
||||
private val magiskRepo: MagiskRepository by inject()
|
||||
|
||||
override fun doWork(): ListenableWorker.Result {
|
||||
val config = runCatching { magiskRepo.fetchConfig().blockingGet() }
|
||||
config.getOrNull()?.let { checkUpdates(it) }
|
||||
return when {
|
||||
config.isFailure -> ListenableWorker.Result.failure()
|
||||
else -> ListenableWorker.Result.success()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkUpdates(config: MagiskConfig) {
|
||||
when {
|
||||
BuildConfig.VERSION_CODE < config.app.versionCode.toIntOrNull() ?: -1 -> Notifications.managerUpdate()
|
||||
Config.magiskVersionCode < config.magisk.versionCode.toIntOrNull() ?: -1 -> Notifications.magiskUpdate()
|
||||
// Make sure shell initializer was ran
|
||||
Shell.getShell()
|
||||
return runCatching {
|
||||
magiskRepo.fetchUpdate().blockingGet()
|
||||
if (BuildConfig.VERSION_CODE < Info.remote.app.versionCode)
|
||||
Notifications.managerUpdate(applicationContext)
|
||||
else if (Info.magiskVersionCode < Info.remote.magisk.versionCode)
|
||||
Notifications.magiskUpdate(applicationContext)
|
||||
ListenableWorker.Result.success()
|
||||
}.getOrElse {
|
||||
ListenableWorker.Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,84 +0,0 @@
|
||||
package com.topjohnwu.magisk.model.worker;
|
||||
|
||||
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;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.work.Data;
|
||||
import androidx.work.ListenableWorker;
|
||||
|
||||
public abstract class DelegateWorker {
|
||||
|
||||
private ListenableWorker worker;
|
||||
|
||||
@NonNull
|
||||
public abstract ListenableWorker.Result doWork();
|
||||
|
||||
public void onStopped() {}
|
||||
|
||||
public void setActualWorker(ListenableWorker w) {
|
||||
worker = w;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Context getApplicationContext() {
|
||||
return worker.getApplicationContext();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public UUID getId() {
|
||||
return worker.getId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Data getInputData() {
|
||||
return worker.getInputData();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Set<String> getTags() {
|
||||
return worker.getTags();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@RequiresApi(24)
|
||||
public List<Uri> getTriggeredContentUris() {
|
||||
return worker.getTriggeredContentUris();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@RequiresApi(24)
|
||||
public List<String> getTriggeredContentAuthorities() {
|
||||
return worker.getTriggeredContentAuthorities();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@RequiresApi(28)
|
||||
public Network getNetwork() {
|
||||
return worker.getNetwork();
|
||||
}
|
||||
|
||||
public int getRunAttemptCount() {
|
||||
return worker.getRunAttemptCount();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@MainThread
|
||||
public ListenableFuture<ListenableWorker.Result> startWork() {
|
||||
return worker.startWork();
|
||||
}
|
||||
|
||||
public boolean isStopped() {
|
||||
return worker.isStopped();
|
||||
}
|
||||
}
|
@@ -0,0 +1,59 @@
|
||||
package com.topjohnwu.magisk.model.worker
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
import android.net.Uri
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.work.Data
|
||||
import androidx.work.ListenableWorker
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import java.util.*
|
||||
|
||||
abstract class DelegateWorker {
|
||||
|
||||
private lateinit var worker: ListenableWorker
|
||||
|
||||
val applicationContext: Context
|
||||
get() = worker.applicationContext
|
||||
|
||||
val id: UUID
|
||||
get() = worker.id
|
||||
|
||||
val inputData: Data
|
||||
get() = worker.inputData
|
||||
|
||||
val tags: Set<String>
|
||||
get() = worker.tags
|
||||
|
||||
val triggeredContentUris: List<Uri>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentUris
|
||||
|
||||
val triggeredContentAuthorities: List<String>
|
||||
@RequiresApi(24)
|
||||
get() = worker.triggeredContentAuthorities
|
||||
|
||||
val network: Network?
|
||||
@RequiresApi(28)
|
||||
get() = worker.network
|
||||
|
||||
val runAttemptCount: Int
|
||||
get() = worker.runAttemptCount
|
||||
|
||||
val isStopped: Boolean
|
||||
get() = worker.isStopped
|
||||
|
||||
abstract fun doWork(): ListenableWorker.Result
|
||||
|
||||
fun onStopped() {}
|
||||
|
||||
fun attachWorker(w: ListenableWorker) {
|
||||
worker = w
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun startWork(): ListenableFuture<ListenableWorker.Result> {
|
||||
return worker.startWork()
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
package com.topjohnwu.magisk.model.zip
|
||||
|
||||
import com.topjohnwu.magisk.utils.forEach
|
||||
import com.topjohnwu.magisk.utils.withStreams
|
||||
import com.topjohnwu.magisk.extensions.forEach
|
||||
import com.topjohnwu.magisk.extensions.withStreams
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import java.io.File
|
||||
import java.util.zip.ZipInputStream
|
||||
|
@@ -1,12 +1,12 @@
|
||||
package com.topjohnwu.magisk.tasks
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.skoumal.teanity.extensions.subscribeK
|
||||
import com.topjohnwu.magisk.App
|
||||
import com.topjohnwu.magisk.Const
|
||||
import com.topjohnwu.magisk.utils.fileName
|
||||
import com.topjohnwu.magisk.utils.inject
|
||||
import com.topjohnwu.magisk.utils.readUri
|
||||
import com.topjohnwu.magisk.extensions.fileName
|
||||
import com.topjohnwu.magisk.extensions.inject
|
||||
import com.topjohnwu.magisk.extensions.readUri
|
||||
import com.topjohnwu.magisk.utils.unzip
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import io.reactivex.Single
|
||||
@@ -20,8 +20,11 @@ abstract class FlashZip(
|
||||
private val logs: MutableList<String>
|
||||
) {
|
||||
|
||||
private val app: App by inject()
|
||||
private val tmpFile: File = File(app.cacheDir, "install.zip")
|
||||
private val context: Context by inject()
|
||||
private val installFolder = File(context.cacheDir, "flash").apply {
|
||||
if (!exists()) mkdirs()
|
||||
}
|
||||
private val tmpFile: File = File(installFolder, "install.zip")
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun unzipAndCheck(): Boolean {
|
||||
@@ -40,7 +43,7 @@ abstract class FlashZip(
|
||||
console.add("- Copying zip to temp directory")
|
||||
|
||||
runCatching {
|
||||
app.readUri(mUri).use { input ->
|
||||
context.readUri(mUri).use { input ->
|
||||
tmpFile.outputStream().use { out -> input.copyTo(out) }
|
||||
}
|
||||
}.getOrElse {
|
||||
|
@@ -1,380 +0,0 @@
|
||||
package com.topjohnwu.magisk.tasks;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.topjohnwu.magisk.App;
|
||||
import com.topjohnwu.magisk.Config;
|
||||
import com.topjohnwu.magisk.Const;
|
||||
import com.topjohnwu.magisk.utils.Utils;
|
||||
import com.topjohnwu.net.DownloadProgressListener;
|
||||
import com.topjohnwu.net.Networking;
|
||||
import com.topjohnwu.signing.SignBoot;
|
||||
import com.topjohnwu.superuser.Shell;
|
||||
import com.topjohnwu.superuser.ShellUtils;
|
||||
import com.topjohnwu.superuser.internal.NOPList;
|
||||
import com.topjohnwu.superuser.internal.UiThreadHandler;
|
||||
import com.topjohnwu.superuser.io.SuFile;
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream;
|
||||
import com.topjohnwu.superuser.io.SuFileOutputStream;
|
||||
|
||||
import org.kamranzafar.jtar.TarEntry;
|
||||
import org.kamranzafar.jtar.TarHeader;
|
||||
import org.kamranzafar.jtar.TarInputStream;
|
||||
import org.kamranzafar.jtar.TarOutputStream;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
public abstract class MagiskInstaller {
|
||||
|
||||
protected String srcBoot;
|
||||
protected File destFile;
|
||||
protected File installDir;
|
||||
|
||||
private List<String> console, logs;
|
||||
private boolean isTar = false;
|
||||
|
||||
private class ProgressLog implements DownloadProgressListener {
|
||||
|
||||
private int prev = -1;
|
||||
private int location;
|
||||
|
||||
@Override
|
||||
public void onProgress(long bytesDownloaded, long totalBytes) {
|
||||
if (prev < 0) {
|
||||
location = console.size();
|
||||
console.add("... 0%");
|
||||
}
|
||||
int curr = (int) (100 * bytesDownloaded / totalBytes);
|
||||
if (prev != curr) {
|
||||
prev = curr;
|
||||
console.set(location, "... " + prev + "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected MagiskInstaller() {
|
||||
console = NOPList.getInstance();
|
||||
logs = NOPList.getInstance();
|
||||
}
|
||||
|
||||
public MagiskInstaller(List<String> out, List<String> err) {
|
||||
console = out;
|
||||
logs = err;
|
||||
installDir = new File(App.deContext.getFilesDir().getParent(), "install");
|
||||
Shell.sh("rm -rf " + installDir).exec();
|
||||
installDir.mkdirs();
|
||||
}
|
||||
|
||||
protected boolean findImage() {
|
||||
srcBoot = ShellUtils.fastCmd("find_boot_image", "echo \"$BOOTIMAGE\"");
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image");
|
||||
return false;
|
||||
}
|
||||
console.add("- Target image: " + srcBoot);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean findSecondaryImage() {
|
||||
String slot = ShellUtils.fastCmd("echo $SLOT");
|
||||
String target = (TextUtils.equals(slot, "_a") ? "_b" : "_a");
|
||||
console.add("- Target slot: " + target);
|
||||
srcBoot = ShellUtils.fastCmd(
|
||||
"SLOT=" + target,
|
||||
"find_boot_image",
|
||||
"SLOT=" + slot,
|
||||
"echo \"$BOOTIMAGE\""
|
||||
);
|
||||
if (srcBoot.isEmpty()) {
|
||||
console.add("! Unable to detect target image");
|
||||
return false;
|
||||
}
|
||||
console.add("- Target image: " + srcBoot);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean extractZip() {
|
||||
String arch;
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
List<String> abis = Arrays.asList(Build.SUPPORTED_ABIS);
|
||||
arch = abis.contains("x86") ? "x86" : "arm";
|
||||
} else {
|
||||
arch = TextUtils.equals(Build.CPU_ABI, "x86") ? "x86" : "arm";
|
||||
}
|
||||
|
||||
console.add("- Device platform: " + Build.CPU_ABI);
|
||||
|
||||
File zip = new File(App.self.getCacheDir(), "magisk.zip");
|
||||
|
||||
if (!ShellUtils.checkSum("MD5", zip, Config.magiskMD5)) {
|
||||
console.add("- Downloading zip");
|
||||
Networking.get(Config.magiskLink)
|
||||
.setDownloadProgressListener(new ProgressLog())
|
||||
.execForFile(zip);
|
||||
} else {
|
||||
console.add("- Existing zip found");
|
||||
}
|
||||
|
||||
try {
|
||||
ZipInputStream zi = new ZipInputStream(new BufferedInputStream(
|
||||
new FileInputStream(zip), (int) zip.length()));
|
||||
ZipEntry ze;
|
||||
while ((ze = zi.getNextEntry()) != null) {
|
||||
if (ze.isDirectory())
|
||||
continue;
|
||||
String name = null;
|
||||
String[] names = { arch + "/", "common/", "META-INF/com/google/android/update-binary" };
|
||||
for (String n : names) {
|
||||
if (ze.getName().startsWith(n)) {
|
||||
name = ze.getName().substring(ze.getName().lastIndexOf('/') + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name == null && ze.getName().startsWith("chromeos/"))
|
||||
name = ze.getName();
|
||||
if (name == null)
|
||||
continue;
|
||||
File dest = (installDir instanceof SuFile) ?
|
||||
new SuFile(installDir, name) :
|
||||
new File(installDir, name);
|
||||
dest.getParentFile().mkdirs();
|
||||
try (OutputStream out = new SuFileOutputStream(dest)) {
|
||||
ShellUtils.pump(zi, out);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
console.add("! Cannot unzip zip");
|
||||
return false;
|
||||
}
|
||||
|
||||
File init64 = SuFile.open(installDir, "magiskinit64");
|
||||
if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_64_BIT_ABIS.length != 0) {
|
||||
init64.renameTo(SuFile.open(installDir, "magiskinit"));
|
||||
} else {
|
||||
init64.delete();
|
||||
}
|
||||
Shell.sh("cd " + installDir, "chmod 755 *").exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
private TarEntry newEntry(String name, long size) {
|
||||
console.add("-- Writing: " + name);
|
||||
return new TarEntry(TarHeader.createHeader(name, size, 0, false, 0644));
|
||||
}
|
||||
|
||||
private void handleTar(InputStream in) throws IOException {
|
||||
console.add("- Processing tar file");
|
||||
boolean vbmeta = false;
|
||||
try (TarInputStream tarIn = new TarInputStream(in);
|
||||
TarOutputStream tarOut = new TarOutputStream(destFile)) {
|
||||
TarEntry entry;
|
||||
while ((entry = tarIn.getNextEntry()) != null) {
|
||||
if (entry.getName().contains("boot.img")
|
||||
|| entry.getName().contains("recovery.img")) {
|
||||
String name = entry.getName();
|
||||
console.add("-- Extracting: " + name);
|
||||
File extract = new File(installDir, name);
|
||||
try (FileOutputStream fout = new FileOutputStream(extract)) {
|
||||
ShellUtils.pump(tarIn, fout);
|
||||
}
|
||||
if (name.contains(".lz4")) {
|
||||
console.add("-- Decompressing: " + name);
|
||||
Shell.sh("./magiskboot --decompress " + extract).to(console).exec();
|
||||
}
|
||||
} else if (entry.getName().contains("vbmeta.img")) {
|
||||
vbmeta = true;
|
||||
ByteBuffer buf = ByteBuffer.allocate(256);
|
||||
buf.put("AVB0".getBytes()); // magic
|
||||
buf.putInt(1); // required_libavb_version_major
|
||||
buf.putInt(120, 2); // flags
|
||||
buf.position(128); // release_string
|
||||
buf.put("avbtool 1.1.0".getBytes());
|
||||
tarOut.putNextEntry(newEntry("vbmeta.img", 256));
|
||||
tarOut.write(buf.array());
|
||||
} else {
|
||||
console.add("-- Writing: " + entry.getName());
|
||||
tarOut.putNextEntry(entry);
|
||||
ShellUtils.pump(tarIn, tarOut);
|
||||
}
|
||||
}
|
||||
File boot = SuFile.open(installDir, "boot.img");
|
||||
File recovery = SuFile.open(installDir, "recovery.img");
|
||||
if (vbmeta && recovery.exists() && boot.exists()) {
|
||||
// Install Magisk to recovery
|
||||
srcBoot = recovery.getPath();
|
||||
// Repack boot image to prevent restore
|
||||
Shell.sh(
|
||||
"./magiskboot --unpack boot.img",
|
||||
"./magiskboot --repack boot.img",
|
||||
"./magiskboot --cleanup",
|
||||
"mv new-boot.img boot.img").exec();
|
||||
try (InputStream sin = new SuFileInputStream(boot)) {
|
||||
tarOut.putNextEntry(newEntry("boot.img", boot.length()));
|
||||
ShellUtils.pump(sin, tarOut);
|
||||
}
|
||||
boot.delete();
|
||||
} else {
|
||||
if (!boot.exists()) {
|
||||
console.add("! No boot image found");
|
||||
throw new IOException();
|
||||
}
|
||||
srcBoot = boot.getPath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean handleFile(Uri uri) {
|
||||
try (InputStream in = new BufferedInputStream(App.self.getContentResolver().openInputStream(uri))) {
|
||||
in.mark(500);
|
||||
byte[] magic = new byte[5];
|
||||
if (in.skip(257) != 257 || in.read(magic) != magic.length) {
|
||||
console.add("! Invalid file");
|
||||
return false;
|
||||
}
|
||||
in.reset();
|
||||
if (Arrays.equals(magic, "ustar".getBytes())) {
|
||||
isTar = true;
|
||||
destFile = new File(Const.EXTERNAL_PATH, "magisk_patched.tar");
|
||||
handleTar(in);
|
||||
} else {
|
||||
// Raw image
|
||||
srcBoot = new File(installDir, "boot.img").getPath();
|
||||
destFile = new File(Const.EXTERNAL_PATH, "magisk_patched.img");
|
||||
console.add("- Copying image to cache");
|
||||
try (OutputStream out = new FileOutputStream(srcBoot)) {
|
||||
ShellUtils.pump(in, out);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
console.add("! Process error");
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean patchBoot() {
|
||||
boolean isSigned;
|
||||
try (InputStream in = new SuFileInputStream(srcBoot)) {
|
||||
isSigned = SignBoot.verifySignature(in, null);
|
||||
if (isSigned) {
|
||||
console.add("- Boot image is signed with AVB 1.0");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
console.add("! Unable to check signature");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Shell.sh(Utils.fmt(
|
||||
"KEEPFORCEENCRYPT=%b KEEPVERITY=%b RECOVERYMODE=%b " +
|
||||
"sh update-binary sh boot_patch.sh %s",
|
||||
Config.keepEnc, Config.keepVerity, Config.recovery, srcBoot))
|
||||
.to(console, logs).exec().isSuccess())
|
||||
return false;
|
||||
|
||||
Shell.Job job = Shell.sh("./magiskboot --cleanup",
|
||||
"mv bin/busybox busybox",
|
||||
"rm -rf magisk.apk bin boot.img update-binary",
|
||||
"cd /");
|
||||
|
||||
File patched = new File(installDir, "new-boot.img");
|
||||
if (isSigned) {
|
||||
console.add("- Signing boot image with test keys");
|
||||
File signed = new File(installDir, "signed.img");
|
||||
try (InputStream in = new SuFileInputStream(patched);
|
||||
OutputStream out = new BufferedOutputStream(new FileOutputStream(signed))) {
|
||||
SignBoot.doSignature("/boot", in, out, null, null);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
job.add("mv -f " + signed + " " + patched);
|
||||
}
|
||||
job.exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean flashBoot() {
|
||||
if (!Shell.su(Utils.fmt("direct_install %s %s", installDir, srcBoot))
|
||||
.to(console, logs).exec().isSuccess())
|
||||
return false;
|
||||
if (!Config.keepVerity)
|
||||
Shell.su("patch_dtbo_image").to(console, logs).exec();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean storeBoot() {
|
||||
File patched = SuFile.open(installDir, "new-boot.img");
|
||||
try {
|
||||
OutputStream os;
|
||||
if (isTar) {
|
||||
os = new TarOutputStream(destFile, true);
|
||||
((TarOutputStream) os).putNextEntry(newEntry(
|
||||
srcBoot.contains("recovery") ? "recovery.img" : "boot.img",
|
||||
patched.length()));
|
||||
} else {
|
||||
os = new BufferedOutputStream(new FileOutputStream(destFile));
|
||||
}
|
||||
try (InputStream in = new SuFileInputStream(patched);
|
||||
OutputStream out = os) {
|
||||
ShellUtils.pump(in, out);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
console.add("! Failed to output to " + destFile);
|
||||
e.printStackTrace();
|
||||
}
|
||||
patched.delete();
|
||||
console.add("");
|
||||
console.add("****************************");
|
||||
console.add(" Output file is placed in ");
|
||||
console.add(" " + destFile + " ");
|
||||
console.add("****************************");
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean postOTA() {
|
||||
SuFile bootctl = new SuFile("/data/adb/bootctl");
|
||||
try (InputStream in = Networking.get(Const.Url.BOOTCTL_URL).execForInputStream().getResult();
|
||||
OutputStream out = new SuFileOutputStream(bootctl)) {
|
||||
ShellUtils.pump(in, out);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
Shell.su("post_ota " + bootctl.getParent()).exec();
|
||||
console.add("***************************************");
|
||||
console.add(" Next reboot will boot to second slot!");
|
||||
console.add("***************************************");
|
||||
return true;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract boolean operations();
|
||||
|
||||
@MainThread
|
||||
protected abstract void onResult(boolean success);
|
||||
|
||||
public void exec() {
|
||||
App.THREAD_POOL.execute(() -> {
|
||||
boolean b = operations();
|
||||
UiThreadHandler.run(() -> onResult(b));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user