mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-12-12 15:50:22 -08:00
Compare commits
267 Commits
manager-v3
...
manager-v5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dc9419d2e | ||
|
|
d2bcac813e | ||
|
|
080c37a7f6 | ||
|
|
f9a3838db6 | ||
|
|
1e61db104b | ||
|
|
30a9c7718d | ||
|
|
34b052b5d3 | ||
|
|
aaa12853ad | ||
|
|
b0ab55b0bf | ||
|
|
d2f8496f4e | ||
|
|
1a69b16d36 | ||
|
|
b5e8673e62 | ||
|
|
264c6a50b6 | ||
|
|
493642eb38 | ||
|
|
28d42b9164 | ||
|
|
42f29062ca | ||
|
|
c4377ed6c2 | ||
|
|
7d283ed65f | ||
|
|
bf1f941e50 | ||
|
|
789fef34ba | ||
|
|
1daf5a611c | ||
|
|
6aed1db67e | ||
|
|
cf68854770 | ||
|
|
711392c73b | ||
|
|
9573c32481 | ||
|
|
a15f80f79d | ||
|
|
23e7475f06 | ||
|
|
1eb571b787 | ||
|
|
dd3b716d85 | ||
|
|
28649c07e3 | ||
|
|
961e02be0d | ||
|
|
a161491bfd | ||
|
|
e0b4d1c1e4 | ||
|
|
fd4aaab137 | ||
|
|
42d14d5ca2 | ||
|
|
d3ff482c9b | ||
|
|
f682368eeb | ||
|
|
4a5d033efb | ||
|
|
343161b195 | ||
|
|
bc576a9659 | ||
|
|
19e407fcc4 | ||
|
|
bc7327d004 | ||
|
|
666fa1c797 | ||
|
|
0eda4a7821 | ||
|
|
862058fd2b | ||
|
|
69e5bcd57d | ||
|
|
efeddda328 | ||
|
|
ff6938280e | ||
|
|
1e4425b30f | ||
|
|
b5d1d8cdad | ||
|
|
029be5ccca | ||
|
|
29c2d785b5 | ||
|
|
abda8cfa32 | ||
|
|
44e7d79d4c | ||
|
|
9a1dc8ee0e | ||
|
|
27879c3f01 | ||
|
|
29096eb5d7 | ||
|
|
a573baea03 | ||
|
|
5af07c4531 | ||
|
|
44e36feb09 | ||
|
|
2a7d996881 | ||
|
|
738f943a68 | ||
|
|
47e62a5681 | ||
|
|
1ecbfd7590 | ||
|
|
67c139a04b | ||
|
|
31cc008249 | ||
|
|
9cb026439d | ||
|
|
e6f10176c6 | ||
|
|
0917c79470 | ||
|
|
597baa986d | ||
|
|
75cc4b4843 | ||
|
|
aac088d496 | ||
|
|
a822e5bbc5 | ||
|
|
c527249c21 | ||
|
|
9ef798f534 | ||
|
|
e69b99f089 | ||
|
|
55b8079e86 | ||
|
|
e272dbe9af | ||
|
|
962f8354ac | ||
|
|
20e4a960f7 | ||
|
|
82249cb50a | ||
|
|
fad417e553 | ||
|
|
5ba692f50c | ||
|
|
907e01e524 | ||
|
|
b8ed23efa7 | ||
|
|
2b3bbf7e67 | ||
|
|
464fe627a3 | ||
|
|
6a9e39c470 | ||
|
|
7fec9a3cc6 | ||
|
|
008f6ef462 | ||
|
|
2440c108ca | ||
|
|
430baad8a4 | ||
|
|
51132e74b4 | ||
|
|
a4f33e106a | ||
|
|
baba3190e0 | ||
|
|
47b13aa5ea | ||
|
|
ae88d3054d | ||
|
|
411b600e14 | ||
|
|
0a0ad9a184 | ||
|
|
234bead59e | ||
|
|
76de310986 | ||
|
|
817f050bcd | ||
|
|
60ae685d1e | ||
|
|
4c7bdbb284 | ||
|
|
435251ca41 | ||
|
|
324a0dd38f | ||
|
|
cc77d93918 | ||
|
|
0ea7d8bd8c | ||
|
|
849b217143 | ||
|
|
9af6efba59 | ||
|
|
079d6f06ef | ||
|
|
9cf0757689 | ||
|
|
b54c438948 | ||
|
|
c3ff4bfdad | ||
|
|
5d62e066e2 | ||
|
|
e94219c5a3 | ||
|
|
8ed9634adf | ||
|
|
0aefa9599f | ||
|
|
e279cf0575 | ||
|
|
a3f0ef8e77 | ||
|
|
8eba05ed4a | ||
|
|
2f78155723 | ||
|
|
6785221479 | ||
|
|
9bc410dd3d | ||
|
|
2491ab6bf9 | ||
|
|
f615ed40cd | ||
|
|
430f2cafc1 | ||
|
|
0ad049da88 | ||
|
|
2c7691567b | ||
|
|
1d70d0fe94 | ||
|
|
ac44f05811 | ||
|
|
d99252f394 | ||
|
|
b58c7ba7c5 | ||
|
|
8c5acd1a0a | ||
|
|
b9b1ebf18c | ||
|
|
8ca132cef0 | ||
|
|
a03bb90754 | ||
|
|
d1c939f48a | ||
|
|
21b11f1b48 | ||
|
|
23c84a7803 | ||
|
|
f9ab060403 | ||
|
|
df7a5bf149 | ||
|
|
c4afa069df | ||
|
|
1bfafdb44f | ||
|
|
1ef5bd7076 | ||
|
|
29176fa4f4 | ||
|
|
958c95732b | ||
|
|
44b0d4127c | ||
|
|
1418ec2416 | ||
|
|
b51978f51c | ||
|
|
b07361580a | ||
|
|
d1b5ebad7d | ||
|
|
f4ce813de9 | ||
|
|
b44ac994d8 | ||
|
|
333948814c | ||
|
|
1a51ad6e01 | ||
|
|
22a5c11f0d | ||
|
|
51b22d1ad4 | ||
|
|
bef5969580 | ||
|
|
c6bf7bb9cd | ||
|
|
2a84d92cbf | ||
|
|
62de36b0da | ||
|
|
03a9aaeff7 | ||
|
|
45765e292d | ||
|
|
6e28a26015 | ||
|
|
9150bf720d | ||
|
|
845864679c | ||
|
|
b3b2149ebb | ||
|
|
0886dca385 | ||
|
|
53198ba4a7 | ||
|
|
a9652ee1fd | ||
|
|
75caf2f01c | ||
|
|
65bab2666e | ||
|
|
6d93ae399a | ||
|
|
7239c2e31a | ||
|
|
43b7ef8110 | ||
|
|
99ef0b8cb4 | ||
|
|
0efb4da0ee | ||
|
|
ed7920d61e | ||
|
|
c0379c8e25 | ||
|
|
00a0e64fdd | ||
|
|
0dc60debea | ||
|
|
c44ae5888c | ||
|
|
b9495cd1bb | ||
|
|
bfec381933 | ||
|
|
2dddb8df69 | ||
|
|
d30397e9c0 | ||
|
|
d9597549fd | ||
|
|
13512b4146 | ||
|
|
49e546919a | ||
|
|
586015c2ed | ||
|
|
4a7e067d1a | ||
|
|
9bc0b7f183 | ||
|
|
cd4dfc9861 | ||
|
|
09bdbc1224 | ||
|
|
978b3a64c5 | ||
|
|
651547ef20 | ||
|
|
b4d95977d0 | ||
|
|
5d8bb897db | ||
|
|
84c8ecb372 | ||
|
|
61abe5b948 | ||
|
|
a5b573eaaa | ||
|
|
cbb32f82eb | ||
|
|
ca9334b2df | ||
|
|
959ed7f866 | ||
|
|
a5c0411be0 | ||
|
|
32e1303742 | ||
|
|
7263b6fe89 | ||
|
|
46a4070f84 | ||
|
|
c3c155a1ed | ||
|
|
b067105660 | ||
|
|
15ca18848e | ||
|
|
67c9e2ead6 | ||
|
|
3681177be4 | ||
|
|
6eb814ef0b | ||
|
|
bcc695234c | ||
|
|
ad16a6fc1b | ||
|
|
478b7eeb65 | ||
|
|
151a153dc9 | ||
|
|
ad131854ca | ||
|
|
0bd0eb9e59 | ||
|
|
cf16fd0104 | ||
|
|
21b00ac6ca | ||
|
|
57e6f3080c | ||
|
|
89744100ce | ||
|
|
a718f9bbfd | ||
|
|
e81bc4f044 | ||
|
|
4dbacd79ae | ||
|
|
ae74d54451 | ||
|
|
dc316c5669 | ||
|
|
e9f04256c9 | ||
|
|
e1aabd70e8 | ||
|
|
a9dc1b32e0 | ||
|
|
01d847ae4e | ||
|
|
61e2c3444a | ||
|
|
5363b0f810 | ||
|
|
f0e1a8823e | ||
|
|
7be5937aa0 | ||
|
|
8f43055b0e | ||
|
|
953a81b299 | ||
|
|
1d34ae7934 | ||
|
|
2cabb2666b | ||
|
|
0b59bb1a29 | ||
|
|
c1e7d74b96 | ||
|
|
cc262d6595 | ||
|
|
61d43b118b | ||
|
|
989d8181dd | ||
|
|
cffc157d98 | ||
|
|
2a70619577 | ||
|
|
b91919bffa | ||
|
|
fb7a4bf880 | ||
|
|
4b41799a90 | ||
|
|
123f39a21b | ||
|
|
cadab12737 | ||
|
|
742055c43b | ||
|
|
fa73b41fa7 | ||
|
|
a474eafe84 | ||
|
|
442fcf921c | ||
|
|
fb0923f3ab | ||
|
|
5bb943f845 | ||
|
|
a3109953d0 | ||
|
|
ff266c8c79 | ||
|
|
ef2e02098d | ||
|
|
93598d3a51 | ||
|
|
53aebcfb1e | ||
|
|
bb2467d2ac | ||
|
|
05c063b61d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,6 +3,6 @@
|
|||||||
/local.properties
|
/local.properties
|
||||||
.idea/
|
.idea/
|
||||||
/build
|
/build
|
||||||
app/app-release.apk
|
app/release
|
||||||
*.hprof
|
*.hprof
|
||||||
app/.externalNativeBuild/
|
app/.externalNativeBuild/
|
||||||
|
|||||||
10
README.md
10
README.md
@@ -1,4 +1,6 @@
|
|||||||
# Magisk Manager
|
# Magisk Manager
|
||||||
The project can only be compiled on Android Studio Version 2.2.0+
|
You need to install CMake and NDK to build the zipadjust library for zip preprocessing
|
||||||
I use Java 8 features, which requires Jack compiler and that's only available in 2.2.0+
|
|
||||||
Also, you need to install CMake and NDK to build the zipadjust library for zip preprocessing
|
## Pre-built Binaries
|
||||||
|
Busybox (arm and x86) compiled by osm0sis (`libbusybox.so` under `app\src\main\jniLibs`)
|
||||||
|
Source and more info: [osm0sis' Odds and Ends](https://forum.xda-developers.com/showthread.php?t=2239421)
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.6)
|
|
||||||
add_library(zipadjust SHARED src/main/jni/zipadjust.c)
|
|
||||||
find_library(libz z)
|
|
||||||
find_library(liblog log)
|
|
||||||
target_link_libraries(zipadjust ${libz} ${liblog})
|
|
||||||
@@ -1,31 +1,25 @@
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 25
|
compileSdkVersion 26
|
||||||
buildToolsVersion "25.0.2"
|
buildToolsVersion "26.0.0"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.topjohnwu.magisk"
|
applicationId "com.topjohnwu.magisk"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 25
|
targetSdkVersion 26
|
||||||
versionCode 11
|
versionCode 45
|
||||||
versionName "3.0"
|
versionName "5.0.5"
|
||||||
jackOptions {
|
|
||||||
enabled true
|
|
||||||
}
|
|
||||||
ndk {
|
ndk {
|
||||||
moduleName 'zipadjust'
|
moduleName 'zipadjust'
|
||||||
abiFilters 'x86', 'x86_64', 'armeabi', 'arm64-v8a'
|
abiFilters 'x86', 'armeabi-v7a'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
incremental false
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
|
shrinkResources true
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,31 +28,36 @@ android {
|
|||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
dexOptions {
|
dexOptions {
|
||||||
preDexLibraries = false
|
preDexLibraries true
|
||||||
|
javaMaxHeapSize "2g"
|
||||||
}
|
}
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
path 'CMakeLists.txt'
|
path 'src/main/jni/CMakeLists.txt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
lintOptions {
|
||||||
|
disable 'MissingTranslation'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
maven { url "https://jitpack.io" }
|
maven { url "https://jitpack.io" }
|
||||||
|
maven { url "https://maven.google.com" }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile fileTree(include: ['*.jar'], dir: 'libs')
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
compile 'com.android.support:recyclerview-v7:25.1.0'
|
implementation 'com.android.support:recyclerview-v7:26.0.0-beta2'
|
||||||
compile 'com.android.support:cardview-v7:25.1.0'
|
implementation 'com.android.support:cardview-v7:26.0.0-beta2'
|
||||||
compile 'com.android.support:design:25.1.0'
|
implementation 'com.android.support:design:26.0.0-beta2'
|
||||||
compile 'com.jakewharton:butterknife:8.4.0'
|
implementation 'com.android.support:support-v4:26.0.0-beta2'
|
||||||
compile 'com.google.code.gson:gson:2.8.0'
|
implementation 'com.jakewharton:butterknife:8.7.0'
|
||||||
compile 'com.github.clans:fab:1.6.4'
|
implementation 'com.thoughtbot:expandablerecyclerview:1.4'
|
||||||
compile 'com.madgag.spongycastle:core:1.54.0.0'
|
implementation 'us.feras.mdv:markdownview:1.1.0'
|
||||||
compile 'com.madgag.spongycastle:prov:1.54.0.0'
|
implementation 'com.madgag.spongycastle:core:1.54.0.0'
|
||||||
compile 'com.madgag.spongycastle:pkix:1.54.0.0'
|
implementation 'com.madgag.spongycastle:prov:1.54.0.0'
|
||||||
compile 'com.madgag.spongycastle:pg:1.54.0.0'
|
implementation 'com.madgag.spongycastle:pkix:1.54.0.0'
|
||||||
compile 'com.google.android.gms:play-services-safetynet:9.0.1'
|
implementation 'com.google.android.gms:play-services-safetynet:9.0.1'
|
||||||
annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0'
|
annotationProcessor 'com.jakewharton:butterknife-compiler:8.7.0'
|
||||||
}
|
}
|
||||||
|
|||||||
33
app/proguard-rules.pro
vendored
33
app/proguard-rules.pro
vendored
@@ -16,31 +16,10 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
|
||||||
# removes such information by default, so configure it to keep all of it.
|
|
||||||
-keepattributes Signature
|
|
||||||
|
|
||||||
# For using GSON @Expose annotation
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
|
|
||||||
# Gson specific classes
|
|
||||||
-keep class sun.misc.Unsafe { *; }
|
|
||||||
-keep class com.google.gson.** { *; }
|
|
||||||
|
|
||||||
# Application classes that will be serialized/deserialized over Gson
|
|
||||||
-keep class com.topjohnwu.magisk.module.** { *; }
|
|
||||||
-keep class com.topjohnwu.magisk.utils.ModuleHelper$ValueSortedMap { *; }
|
|
||||||
|
|
||||||
# Prevent proguard from stripping interface information from TypeAdapterFactory,
|
|
||||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
|
||||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
|
||||||
-keep class * implements com.google.gson.JsonSerializer
|
|
||||||
-keep class * implements com.google.gson.JsonDeserializer
|
|
||||||
|
|
||||||
-keep class android.support.v7.internal.** { *; }
|
|
||||||
-keep interface android.support.v7.internal.** { *; }
|
|
||||||
-keep class android.support.v7.** { *; }
|
|
||||||
-keep interface android.support.v7.** { *; }
|
|
||||||
|
|
||||||
# SpongyCastle
|
# SpongyCastle
|
||||||
-keep class org.spongycastle.** {*;}
|
-keep class org.spongycastle.** { *; }
|
||||||
|
-dontwarn javax.naming.**
|
||||||
|
|
||||||
|
-dontwarn android.content.**
|
||||||
|
-dontwarn android.animation.**
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest package="com.topjohnwu.magisk"
|
<manifest
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
package="com.topjohnwu.magisk"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
android:name="android.permission.PACKAGE_USAGE_STATS"
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
tools:ignore="ProtectedPermissions" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".MagiskManager"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
@@ -41,6 +41,42 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:theme="@style/AppTheme.Transparent" />
|
android:theme="@style/AppTheme.Transparent" />
|
||||||
|
<activity
|
||||||
|
android:name=".superuser.RequestActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:taskAffinity="internal.superuser"
|
||||||
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
|
<activity
|
||||||
|
android:name=".superuser.SuRequestActivity"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:taskAffinity="internal.superuser"
|
||||||
|
android:theme="@style/SuRequest" />
|
||||||
|
|
||||||
|
<receiver android:name=".superuser.SuReceiver" />
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.BootReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.PackageReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.PACKAGE_REPLACED" />
|
||||||
|
<action android:name="android.intent.action.PACKAGE_FULLY_REMOVED" />
|
||||||
|
<data android:scheme="package" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name=".receivers.ManagerUpdate" />
|
||||||
|
|
||||||
|
<service android:name=".services.OnBootIntentService" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.UpdateCheckService"
|
||||||
|
android:permission="android.permission.BIND_JOB_SERVICE"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="android.support.v4.content.FileProvider"
|
android:name="android.support.v4.content.FileProvider"
|
||||||
|
|||||||
276
app/src/main/assets/dark.css
Normal file
276
app/src/main/assets/dark.css
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
body {
|
||||||
|
font-family: Helvetica, arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: #303030;
|
||||||
|
color: white;
|
||||||
|
padding: 15px; }
|
||||||
|
|
||||||
|
body > *:first-child {
|
||||||
|
margin-top: 0 !important; }
|
||||||
|
body > *:last-child {
|
||||||
|
margin-bottom: 0 !important; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #4183C4; }
|
||||||
|
a.absent {
|
||||||
|
color: #cc0000; }
|
||||||
|
a.anchor {
|
||||||
|
display: block;
|
||||||
|
padding-left: 30px;
|
||||||
|
margin-left: -30px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0; }
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
cursor: text;
|
||||||
|
position: relative; }
|
||||||
|
|
||||||
|
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
|
||||||
|
background: url("../../images/modules/styleguide/para.png") no-repeat 10px center;
|
||||||
|
text-decoration: none; }
|
||||||
|
|
||||||
|
h1 tt, h1 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h2 tt, h2 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h3 tt, h3 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h4 tt, h4 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h5 tt, h5 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h6 tt, h6 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px; }
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #cccccc; }
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px; }
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px; }
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 14px; }
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
color: #888888;
|
||||||
|
font-size: 14px; }
|
||||||
|
|
||||||
|
p, blockquote, ul, ol, dl, li, table, pre {
|
||||||
|
margin: 15px 0; }
|
||||||
|
|
||||||
|
hr {
|
||||||
|
background: transparent url("../../images/modules/pulls/dirty-shade.png") repeat-x 0 0;
|
||||||
|
border: 0 none;
|
||||||
|
color: #cccccc;
|
||||||
|
height: 4px;
|
||||||
|
padding: 0; }
|
||||||
|
|
||||||
|
body > h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
body > h1:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
body > h1:first-child + h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
|
||||||
|
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
|
||||||
|
h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
|
||||||
|
margin-top: 0; }
|
||||||
|
|
||||||
|
li p.first {
|
||||||
|
display: inline-block; }
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 30px; }
|
||||||
|
|
||||||
|
ul :first-child, ol :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
|
||||||
|
ul :last-child, ol :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
dl {
|
||||||
|
padding: 0; }
|
||||||
|
dl dt {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0;
|
||||||
|
margin: 15px 0 5px; }
|
||||||
|
dl dt:first-child {
|
||||||
|
padding: 0; }
|
||||||
|
dl dt > :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
dl dt > :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
dl dd {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
padding: 0 15px; }
|
||||||
|
dl dd > :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
dl dd > :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #404040;
|
||||||
|
padding: 0 15px;
|
||||||
|
color: #888888; }
|
||||||
|
blockquote > :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
blockquote > :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
padding: 0; }
|
||||||
|
table tr {
|
||||||
|
border-top: 1px solid #707070;
|
||||||
|
background-color: #303030;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0; }
|
||||||
|
table tr:nth-child(2n) {
|
||||||
|
background-color: #505050; }
|
||||||
|
table tr th {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #707070;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 13px; }
|
||||||
|
table tr td {
|
||||||
|
border: 1px solid #707070;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 13px; }
|
||||||
|
table tr th :first-child, table tr td :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
table tr th :last-child, table tr td :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%; }
|
||||||
|
|
||||||
|
span.frame {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden; }
|
||||||
|
span.frame > span {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
padding: 7px;
|
||||||
|
width: auto; }
|
||||||
|
span.frame span img {
|
||||||
|
display: block;
|
||||||
|
float: left; }
|
||||||
|
span.frame span span {
|
||||||
|
clear: both;
|
||||||
|
color: #cccccc;
|
||||||
|
display: block;
|
||||||
|
padding: 5px 0 0; }
|
||||||
|
span.align-center {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
clear: both; }
|
||||||
|
span.align-center > span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
text-align: center; }
|
||||||
|
span.align-center span img {
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center; }
|
||||||
|
span.align-right {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
clear: both; }
|
||||||
|
span.align-right > span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
text-align: right; }
|
||||||
|
span.align-right span img {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right; }
|
||||||
|
span.float-left {
|
||||||
|
display: block;
|
||||||
|
margin-right: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
float: left; }
|
||||||
|
span.float-left span {
|
||||||
|
margin: 13px 0 0; }
|
||||||
|
span.float-right {
|
||||||
|
display: block;
|
||||||
|
margin-left: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
float: right; }
|
||||||
|
span.float-right > span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
text-align: right; }
|
||||||
|
|
||||||
|
code, tt {
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid #707070;
|
||||||
|
background-color: #606060;
|
||||||
|
border-radius: 3px; }
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
border: none;
|
||||||
|
background: transparent; }
|
||||||
|
|
||||||
|
.highlight pre {
|
||||||
|
background-color: #3f3f3f;
|
||||||
|
border: 1px solid #707070;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 19px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 3px; }
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #606060;
|
||||||
|
border: 1px solid #707070;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 19px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 3px; }
|
||||||
|
pre code, pre tt {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none; }
|
||||||
277
app/src/main/assets/light.css
Normal file
277
app/src/main/assets/light.css
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
body {
|
||||||
|
font-family: Helvetica, arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
background-color: white;
|
||||||
|
padding: 15px; }
|
||||||
|
|
||||||
|
body > *:first-child {
|
||||||
|
margin-top: 0 !important; }
|
||||||
|
body > *:last-child {
|
||||||
|
margin-bottom: 0 !important; }
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #4183C4; }
|
||||||
|
a.absent {
|
||||||
|
color: #cc0000; }
|
||||||
|
a.anchor {
|
||||||
|
display: block;
|
||||||
|
padding-left: 30px;
|
||||||
|
margin-left: -30px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0; }
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 20px 0 10px;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
cursor: text;
|
||||||
|
position: relative; }
|
||||||
|
|
||||||
|
h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor {
|
||||||
|
background: url("../../images/modules/styleguide/para.png") no-repeat 10px center;
|
||||||
|
text-decoration: none; }
|
||||||
|
|
||||||
|
h1 tt, h1 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h2 tt, h2 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h3 tt, h3 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h4 tt, h4 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h5 tt, h5 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h6 tt, h6 code {
|
||||||
|
font-size: inherit; }
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 28px;
|
||||||
|
color: black; }
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
border-bottom: 1px solid #cccccc;
|
||||||
|
color: black; }
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px; }
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px; }
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 14px; }
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
color: #777777;
|
||||||
|
font-size: 14px; }
|
||||||
|
|
||||||
|
p, blockquote, ul, ol, dl, li, table, pre {
|
||||||
|
margin: 15px 0; }
|
||||||
|
|
||||||
|
hr {
|
||||||
|
background: transparent url("../../images/modules/pulls/dirty-shade.png") repeat-x 0 0;
|
||||||
|
border: 0 none;
|
||||||
|
color: #cccccc;
|
||||||
|
height: 4px;
|
||||||
|
padding: 0; }
|
||||||
|
|
||||||
|
body > h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
body > h1:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
body > h1:first-child + h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
|
||||||
|
a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0; }
|
||||||
|
|
||||||
|
h1 p, h2 p, h3 p, h4 p, h5 p, h6 p {
|
||||||
|
margin-top: 0; }
|
||||||
|
|
||||||
|
li p.first {
|
||||||
|
display: inline-block; }
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
padding-left: 30px; }
|
||||||
|
|
||||||
|
ul :first-child, ol :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
|
||||||
|
ul :last-child, ol :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
dl {
|
||||||
|
padding: 0; }
|
||||||
|
dl dt {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 0;
|
||||||
|
margin: 15px 0 5px; }
|
||||||
|
dl dt:first-child {
|
||||||
|
padding: 0; }
|
||||||
|
dl dt > :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
dl dt > :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
dl dd {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
padding: 0 15px; }
|
||||||
|
dl dd > :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
dl dd > :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #dddddd;
|
||||||
|
padding: 0 15px;
|
||||||
|
color: #777777; }
|
||||||
|
blockquote > :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
blockquote > :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
padding: 0; }
|
||||||
|
table tr {
|
||||||
|
border-top: 1px solid #cccccc;
|
||||||
|
background-color: white;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0; }
|
||||||
|
table tr:nth-child(2n) {
|
||||||
|
background-color: #f8f8f8; }
|
||||||
|
table tr th {
|
||||||
|
font-weight: bold;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 13px; }
|
||||||
|
table tr td {
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 13px; }
|
||||||
|
table tr th :first-child, table tr td :first-child {
|
||||||
|
margin-top: 0; }
|
||||||
|
table tr th :last-child, table tr td :last-child {
|
||||||
|
margin-bottom: 0; }
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%; }
|
||||||
|
|
||||||
|
span.frame {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden; }
|
||||||
|
span.frame > span {
|
||||||
|
border: 1px solid #dddddd;
|
||||||
|
display: block;
|
||||||
|
float: left;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
padding: 7px;
|
||||||
|
width: auto; }
|
||||||
|
span.frame span img {
|
||||||
|
display: block;
|
||||||
|
float: left; }
|
||||||
|
span.frame span span {
|
||||||
|
clear: both;
|
||||||
|
color: #333333;
|
||||||
|
display: block;
|
||||||
|
padding: 5px 0 0; }
|
||||||
|
span.align-center {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
clear: both; }
|
||||||
|
span.align-center > span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
text-align: center; }
|
||||||
|
span.align-center span img {
|
||||||
|
margin: 0 auto;
|
||||||
|
text-align: center; }
|
||||||
|
span.align-right {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
clear: both; }
|
||||||
|
span.align-right > span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px 0 0;
|
||||||
|
text-align: right; }
|
||||||
|
span.align-right span img {
|
||||||
|
margin: 0;
|
||||||
|
text-align: right; }
|
||||||
|
span.float-left {
|
||||||
|
display: block;
|
||||||
|
margin-right: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
float: left; }
|
||||||
|
span.float-left span {
|
||||||
|
margin: 13px 0 0; }
|
||||||
|
span.float-right {
|
||||||
|
display: block;
|
||||||
|
margin-left: 13px;
|
||||||
|
overflow: hidden;
|
||||||
|
float: right; }
|
||||||
|
span.float-right > span {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 13px auto 0;
|
||||||
|
text-align: right; }
|
||||||
|
|
||||||
|
code, tt {
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0 5px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid #eaeaea;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 3px; }
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: pre;
|
||||||
|
border: none;
|
||||||
|
background: transparent; }
|
||||||
|
|
||||||
|
.highlight pre {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 19px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 3px; }
|
||||||
|
|
||||||
|
pre {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px solid #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 19px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 3px; }
|
||||||
|
pre code, pre tt {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none; }
|
||||||
137
app/src/main/assets/magisk_uninstaller.sh
Normal file
137
app/src/main/assets/magisk_uninstaller.sh
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
##########################################################################################
|
||||||
|
#
|
||||||
|
# Magisk Uninstaller
|
||||||
|
# by topjohnwu
|
||||||
|
#
|
||||||
|
# This script can be placed in /cache/magisk_uninstaller.sh
|
||||||
|
# The Magisk main binary will pick up the script, and uninstall itself, following a reboot
|
||||||
|
# This script can also be used in flashable zip with the uninstaller_loader.sh
|
||||||
|
#
|
||||||
|
# This script will try to do restoration with the following:
|
||||||
|
# 1-1. Find and restore the original stock boot image dump (OTA proof)
|
||||||
|
# 1-2. If 1-1 fails, restore ramdisk from the internal backup
|
||||||
|
# (ramdisk fully restored, not OTA friendly)
|
||||||
|
# 1-3. If 1-2 fails, it will remove added files in ramdisk, however modified files
|
||||||
|
# are remained modified, because we have no backups. By doing so, Magisk will
|
||||||
|
# not be started at boot, but this isn't actually 100% cleaned up
|
||||||
|
# 2. Remove all Magisk related files
|
||||||
|
# (The list is LARGE, most likely due to bad decision in early versions
|
||||||
|
# the latest versions has much less bloat to cleanup)
|
||||||
|
#
|
||||||
|
##########################################################################################
|
||||||
|
|
||||||
|
# Call ui_print_wrap if exists, or else simply use echo
|
||||||
|
# Useful when wrapped in flashable zip
|
||||||
|
ui_print_wrap() {
|
||||||
|
type ui_print >/dev/null 2>&1 && ui_print "$1" || echo "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Call abort if exists, or else show error message and exit
|
||||||
|
# Essential when wrapped in flashable zip
|
||||||
|
abort_wrap() {
|
||||||
|
type abort >/dev/null 2>&1
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
ui_print_wrap "$1"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
abort "$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ ! -d $MAGISKBIN -o ! -f $MAGISKBIN/magiskboot -o ! -f $MAGISKBIN/util_functions.sh ]; then
|
||||||
|
ui_print_wrap "! Cannot find $MAGISKBIN"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -z $BOOTMODE ] && BOOTMODE=false
|
||||||
|
|
||||||
|
MAGISKBIN=/data/magisk
|
||||||
|
CHROMEDIR=$MAGISKBIN/chromeos
|
||||||
|
|
||||||
|
# Default permissions
|
||||||
|
umask 022
|
||||||
|
|
||||||
|
# Load utility functions
|
||||||
|
. $MAGISKBIN/util_functions.sh
|
||||||
|
|
||||||
|
# Find the boot image
|
||||||
|
find_boot_image
|
||||||
|
[ -z $BOOTIMAGE ] && abort "! Unable to detect boot image"
|
||||||
|
|
||||||
|
ui_print_wrap "- Found Boot Image: $BOOTIMAGE"
|
||||||
|
|
||||||
|
cd $MAGISKBIN
|
||||||
|
|
||||||
|
ui_print_wrap "- Unpacking boot image"
|
||||||
|
./magiskboot --unpack "$BOOTIMAGE"
|
||||||
|
[ $? -ne 0 ] && abort_wrap "! Unable to unpack boot image"
|
||||||
|
|
||||||
|
# Update our previous backup to new format if exists
|
||||||
|
if [ -f /data/stock_boot.img ]; then
|
||||||
|
SHA1=`./magiskboot --sha1 /data/stock_boot.img | tail -n 1`
|
||||||
|
STOCKDUMP=/data/stock_boot_${SHA1}.img
|
||||||
|
mv /data/stock_boot.img $STOCKDUMP
|
||||||
|
./magiskboot --compress $STOCKDUMP
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Detect boot image state
|
||||||
|
./magiskboot --cpio-test ramdisk.cpio
|
||||||
|
case $? in
|
||||||
|
0 ) # Stock boot
|
||||||
|
ui_print_wrap "- Stock boot image detected!"
|
||||||
|
ui_print_wrap "! Magisk is not installed!"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
1 ) # Magisk patched
|
||||||
|
ui_print_wrap "- Magisk patched image detected!"
|
||||||
|
# Find SHA1 of stock boot image
|
||||||
|
if [ -z $SHA1 ]; then
|
||||||
|
./magiskboot --cpio-extract ramdisk.cpio init.magisk.rc init.magisk.rc.old
|
||||||
|
SHA1=`grep_prop "# STOCKSHA1" init.magisk.rc.old`
|
||||||
|
rm -f init.magisk.rc.old
|
||||||
|
fi
|
||||||
|
[ ! -z $SHA1 ] && STOCKDUMP=/data/stock_boot_${SHA1}.img
|
||||||
|
if [ -f ${STOCKDUMP}.gz ]; then
|
||||||
|
ui_print_wrap "- Boot image backup found!"
|
||||||
|
./magiskboot --decompress ${STOCKDUMP}.gz stock_boot.img
|
||||||
|
else
|
||||||
|
ui_print_wrap "! Boot image backup unavailable"
|
||||||
|
ui_print_wrap "- Restoring ramdisk with backup"
|
||||||
|
./magiskboot --cpio-restore ramdisk.cpio
|
||||||
|
./magiskboot --repack $BOOTIMAGE stock_boot.img
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
2 ) # Other patched
|
||||||
|
ui_print_wrap "! Boot image patched by other programs!"
|
||||||
|
abort_wrap "! Cannot uninstall with this uninstaller"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Sign chromeos boot
|
||||||
|
if [ -f chromeos ]; then
|
||||||
|
echo > empty
|
||||||
|
|
||||||
|
LD_LIBRARY_PATH=$SYSTEMLIB $CHROMEDIR/futility vbutil_kernel --pack stock_boot.img.signed \
|
||||||
|
--keyblock $CHROMEDIR/kernel.keyblock --signprivate $CHROMEDIR/kernel_data_key.vbprivk \
|
||||||
|
--version 1 --vmlinuz stock_boot.img --config empty --arch arm --bootloader empty --flags 0x1
|
||||||
|
|
||||||
|
rm -f empty stock_boot.img
|
||||||
|
mv stock_boot.img.signed stock_boot.img
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui_print_wrap "- Flashing stock/reverted image"
|
||||||
|
if [ -L "$BOOTIMAGE" ]; then
|
||||||
|
dd if=stock_boot.img of="$BOOTIMAGE" bs=4096
|
||||||
|
else
|
||||||
|
cat stock_boot.img /dev/zero | dd of="$BOOTIMAGE" bs=4096 >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -f stock_boot.img
|
||||||
|
|
||||||
|
ui_print_wrap "- Removing Magisk files"
|
||||||
|
rm -rf /cache/magisk.log /cache/last_magisk.log /cache/magiskhide.log /cache/.disable_magisk \
|
||||||
|
/cache/magisk /cache/magisk_merge /cache/magisk_mount /cache/unblock /cache/magisk_uninstaller.sh \
|
||||||
|
/data/Magisk.apk /data/magisk.apk /data/magisk.img /data/magisk_merge.img /data/magisk_debug.log \
|
||||||
|
/data/busybox /data/magisk /data/custom_ramdisk_patch.sh 2>/dev/null
|
||||||
|
|
||||||
|
$BOOTMODE && reboot
|
||||||
192
app/src/main/assets/util_functions.sh
Normal file
192
app/src/main/assets/util_functions.sh
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
##########################################################################################
|
||||||
|
#
|
||||||
|
# Magisk General Utility Functions
|
||||||
|
# by topjohnwu
|
||||||
|
#
|
||||||
|
# Used in flash_script.sh, addon.d.sh, magisk module installers, and uninstaller
|
||||||
|
#
|
||||||
|
##########################################################################################
|
||||||
|
|
||||||
|
get_outfd() {
|
||||||
|
readlink /proc/$$/fd/$OUTFD 2>/dev/null | grep /tmp >/dev/null
|
||||||
|
if [ "$?" -eq "0" ]; then
|
||||||
|
OUTFD=0
|
||||||
|
|
||||||
|
for FD in `ls /proc/$$/fd`; do
|
||||||
|
readlink /proc/$$/fd/$FD 2>/dev/null | grep pipe >/dev/null
|
||||||
|
if [ "$?" -eq "0" ]; then
|
||||||
|
ps | grep " 3 $FD " | grep -v grep >/dev/null
|
||||||
|
if [ "$?" -eq "0" ]; then
|
||||||
|
OUTFD=$FD
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ui_print() {
|
||||||
|
if $BOOTMODE; then
|
||||||
|
echo "$1"
|
||||||
|
else
|
||||||
|
echo -n -e "ui_print $1\n" >> /proc/self/fd/$OUTFD
|
||||||
|
echo -n -e "ui_print\n" >> /proc/self/fd/$OUTFD
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
getvar() {
|
||||||
|
local VARNAME=$1
|
||||||
|
local VALUE=$(eval echo \$"$VARNAME");
|
||||||
|
for FILE in /dev/.magisk /data/.magisk /cache/.magisk /system/.magisk; do
|
||||||
|
if [ -z "$VALUE" ]; then
|
||||||
|
LINE=$(cat $FILE 2>/dev/null | grep "$VARNAME=")
|
||||||
|
if [ ! -z "$LINE" ]; then
|
||||||
|
VALUE=${LINE#*=}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
eval $VARNAME=\$VALUE
|
||||||
|
}
|
||||||
|
|
||||||
|
find_boot_image() {
|
||||||
|
if [ -z "$BOOTIMAGE" ]; then
|
||||||
|
for BLOCK in boot_a BOOT_A kern-a KERN-A android_boot ANDROID_BOOT kernel KERNEL boot BOOT lnx LNX; do
|
||||||
|
BOOTIMAGE=`ls /dev/block/by-name/$BLOCK || ls /dev/block/platform/*/by-name/$BLOCK || ls /dev/block/platform/*/*/by-name/$BLOCK` 2>/dev/null
|
||||||
|
[ ! -z $BOOTIMAGE ] && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
# Recovery fallback
|
||||||
|
if [ -z "$BOOTIMAGE" ]; then
|
||||||
|
for FSTAB in /etc/*fstab*; do
|
||||||
|
BOOTIMAGE=`grep -E '\b/boot\b' $FSTAB | grep -v "#" | grep -oE '/dev/[a-zA-Z0-9_./-]*'`
|
||||||
|
[ ! -z $BOOTIMAGE ] && break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[ -L "$BOOTIMAGE" ] && BOOTIMAGE=`readlink $BOOTIMAGE`
|
||||||
|
}
|
||||||
|
|
||||||
|
is_mounted() {
|
||||||
|
if [ ! -z "$2" ]; then
|
||||||
|
cat /proc/mounts | grep $1 | grep $2, >/dev/null
|
||||||
|
else
|
||||||
|
cat /proc/mounts | grep $1 >/dev/null
|
||||||
|
fi
|
||||||
|
return $?
|
||||||
|
}
|
||||||
|
|
||||||
|
grep_prop() {
|
||||||
|
REGEX="s/^$1=//p"
|
||||||
|
shift
|
||||||
|
FILES=$@
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
FILES='/system/build.prop'
|
||||||
|
fi
|
||||||
|
cat $FILES 2>/dev/null | sed -n "$REGEX" | head -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_system_su() {
|
||||||
|
if [ -f /system/bin/su -o -f /system/xbin/su ] && [ ! -f /su/bin/su ]; then
|
||||||
|
ui_print "! System installed root detected, mount rw :("
|
||||||
|
mount -o rw,remount /system
|
||||||
|
# SuperSU
|
||||||
|
if [ -e /system/bin/.ext/.su ]; then
|
||||||
|
mv -f /system/bin/app_process32_original /system/bin/app_process32 2>/dev/null
|
||||||
|
mv -f /system/bin/app_process64_original /system/bin/app_process64 2>/dev/null
|
||||||
|
mv -f /system/bin/install-recovery_original.sh /system/bin/install-recovery.sh 2>/dev/null
|
||||||
|
cd /system/bin
|
||||||
|
if [ -e app_process64 ]; then
|
||||||
|
ln -sf app_process64 app_process
|
||||||
|
else
|
||||||
|
ln -sf app_process32 app_process
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
rm -rf /system/.pin /system/bin/.ext /system/etc/.installed_su_daemon /system/etc/.has_su_daemon \
|
||||||
|
/system/xbin/daemonsu /system/xbin/su /system/xbin/sugote /system/xbin/sugote-mksh /system/xbin/supolicy \
|
||||||
|
/system/bin/app_process_init /system/bin/su /cache/su /system/lib/libsupol.so /system/lib64/libsupol.so \
|
||||||
|
/system/su.d /system/etc/install-recovery.sh /system/etc/init.d/99SuperSUDaemon /cache/install-recovery.sh \
|
||||||
|
/system/.supersu /cache/.supersu /data/.supersu \
|
||||||
|
/system/app/Superuser.apk /system/app/SuperSU /cache/Superuser.apk 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
api_level_arch_detect() {
|
||||||
|
API=`grep_prop ro.build.version.sdk`
|
||||||
|
ABI=`grep_prop ro.product.cpu.abi | cut -c-3`
|
||||||
|
ABI2=`grep_prop ro.product.cpu.abi2 | cut -c-3`
|
||||||
|
ABILONG=`grep_prop ro.product.cpu.abi`
|
||||||
|
|
||||||
|
ARCH=arm
|
||||||
|
IS64BIT=false
|
||||||
|
if [ "$ABI" = "x86" ]; then ARCH=x86; fi;
|
||||||
|
if [ "$ABI2" = "x86" ]; then ARCH=x86; fi;
|
||||||
|
if [ "$ABILONG" = "arm64-v8a" ]; then ARCH=arm64; IS64BIT=true; fi;
|
||||||
|
if [ "$ABILONG" = "x86_64" ]; then ARCH=x64; IS64BIT=true; fi;
|
||||||
|
}
|
||||||
|
|
||||||
|
recovery_actions() {
|
||||||
|
# TWRP bug fix
|
||||||
|
mount -o bind /dev/urandom /dev/random
|
||||||
|
# Temporarily block out all custom recovery binaries/libs
|
||||||
|
mv /sbin /sbin_tmp
|
||||||
|
# Add all possible library paths
|
||||||
|
OLD_LD_PATH=$LD_LIBRARY_PATH
|
||||||
|
$IS64BIT && export LD_LIBRARY_PATH=/system/lib64:/system/vendor/lib64 || export LD_LIBRARY_PATH=/system/lib:/system/vendor/lib
|
||||||
|
}
|
||||||
|
|
||||||
|
recovery_cleanup() {
|
||||||
|
mv /sbin_tmp /sbin
|
||||||
|
# Clear LD_LIBRARY_PATH
|
||||||
|
export LD_LIBRARY_PATH=$OLD_LD_PATH
|
||||||
|
ui_print "- Unmounting partitions"
|
||||||
|
umount -l /system
|
||||||
|
umount -l /vendor 2>/dev/null
|
||||||
|
umount -l /dev/random
|
||||||
|
}
|
||||||
|
|
||||||
|
abort() {
|
||||||
|
ui_print "$1"
|
||||||
|
mv /sbin_tmp /sbin 2>/dev/null
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
set_perm() {
|
||||||
|
chown $2:$3 $1 || exit 1
|
||||||
|
chmod $4 $1 || exit 1
|
||||||
|
if [ ! -z $5 ]; then
|
||||||
|
chcon $5 $1 2>/dev/null
|
||||||
|
else
|
||||||
|
chcon 'u:object_r:system_file:s0' $1 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
set_perm_recursive() {
|
||||||
|
find $1 -type d 2>/dev/null | while read dir; do
|
||||||
|
set_perm $dir $2 $3 $4 $6
|
||||||
|
done
|
||||||
|
find $1 -type f 2>/dev/null | while read file; do
|
||||||
|
set_perm $file $2 $3 $5 $6
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
mktouch() {
|
||||||
|
mkdir -p ${1%/*}
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
touch $1
|
||||||
|
else
|
||||||
|
echo $2 > $1
|
||||||
|
fi
|
||||||
|
chmod 644 $1
|
||||||
|
}
|
||||||
|
|
||||||
|
request_size_check() {
|
||||||
|
reqSizeM=`du -s $1 | cut -f1`
|
||||||
|
reqSizeM=$((reqSizeM / 1024 + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
image_size_check() {
|
||||||
|
SIZE="`$MAGISKBIN/magisk --imgsize $IMG`"
|
||||||
|
curUsedM=`echo "$SIZE" | cut -d" " -f1`
|
||||||
|
curSizeM=`echo "$SIZE" | cut -d" " -f2`
|
||||||
|
curFreeM=$((curSizeM - curUsedM))
|
||||||
|
}
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 73 KiB |
@@ -3,21 +3,20 @@ package com.topjohnwu.magisk;
|
|||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.components.AboutCardRow;
|
||||||
|
import com.topjohnwu.magisk.components.Activity;
|
||||||
|
import com.topjohnwu.magisk.components.AlertDialogBuilder;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
@@ -25,11 +24,12 @@ import java.io.InputStream;
|
|||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
public class AboutActivity extends AppCompatActivity {
|
public class AboutActivity extends Activity {
|
||||||
|
|
||||||
private static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/MagiskManager";
|
private static final String DONATION_URL = "https://www.paypal.me/topjohnwu";
|
||||||
private static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
|
private static final String XDA_THREAD = "http://forum.xda-developers.com/showthread.php?t=3432382";
|
||||||
private static final String DONATION_URL = "http://topjohnwu.github.io/donate";
|
private static final String SOURCE_CODE_URL = "https://github.com/topjohnwu/MagiskManager";
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
@BindView(R.id.toolbar) Toolbar toolbar;
|
||||||
@BindView(R.id.app_version_info) AboutCardRow appVersionInfo;
|
@BindView(R.id.app_version_info) AboutCardRow appVersionInfo;
|
||||||
@BindView(R.id.app_changelog) AboutCardRow appChangelog;
|
@BindView(R.id.app_changelog) AboutCardRow appChangelog;
|
||||||
@@ -38,15 +38,12 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
@BindView(R.id.app_source_code) AboutCardRow appSourceCode;
|
@BindView(R.id.app_source_code) AboutCardRow appSourceCode;
|
||||||
@BindView(R.id.support_thread) AboutCardRow supportThread;
|
@BindView(R.id.support_thread) AboutCardRow supportThread;
|
||||||
@BindView(R.id.donation) AboutCardRow donation;
|
@BindView(R.id.donation) AboutCardRow donation;
|
||||||
private AlertDialog.Builder builder;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
String theme = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("theme", "");
|
if (getApplicationContext().isDarkTheme) {
|
||||||
Logger.dev("AboutActivity: Theme is " + theme);
|
setTheme(R.style.AppTheme_Transparent_Dark);
|
||||||
if (theme.equals("Dark")) {
|
|
||||||
setTheme(R.style.AppTheme_dh);
|
|
||||||
}
|
}
|
||||||
setContentView(R.layout.activity_about);
|
setContentView(R.layout.activity_about);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
@@ -63,24 +60,17 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
appVersionInfo.setSummary(BuildConfig.VERSION_NAME);
|
appVersionInfo.setSummary(BuildConfig.VERSION_NAME);
|
||||||
|
|
||||||
String changes = null;
|
String changes = null;
|
||||||
try {
|
try (InputStream is = getAssets().open("changelog.html")) {
|
||||||
InputStream is = getAssets().open("changelog.html");
|
|
||||||
int size = is.available();
|
int size = is.available();
|
||||||
|
|
||||||
byte[] buffer = new byte[size];
|
byte[] buffer = new byte[size];
|
||||||
is.read(buffer);
|
is.read(buffer);
|
||||||
is.close();
|
|
||||||
|
|
||||||
changes = new String(buffer);
|
changes = new String(buffer);
|
||||||
} catch (IOException ignored) {
|
} catch (IOException ignored) {
|
||||||
}
|
}
|
||||||
|
|
||||||
appChangelog.removeSummary();
|
appChangelog.removeSummary();
|
||||||
if (theme.equals("Dark")) {
|
|
||||||
builder = new AlertDialog.Builder(this, R.style.AlertDialog_dh);
|
|
||||||
} else {
|
|
||||||
builder = new AlertDialog.Builder(this);
|
|
||||||
}
|
|
||||||
if (changes == null) {
|
if (changes == null) {
|
||||||
appChangelog.setVisibility(View.GONE);
|
appChangelog.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
@@ -91,13 +81,11 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
result = Html.fromHtml(changes);
|
result = Html.fromHtml(changes);
|
||||||
}
|
}
|
||||||
appChangelog.setOnClickListener(v -> {
|
appChangelog.setOnClickListener(v -> {
|
||||||
AlertDialog d = builder
|
AlertDialog d = new AlertDialogBuilder(this)
|
||||||
.setTitle(R.string.app_changelog)
|
.setTitle(R.string.app_changelog)
|
||||||
.setMessage(result)
|
.setMessage(result)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.create();
|
.show();
|
||||||
|
|
||||||
d.show();
|
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
((TextView) d.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
|
((TextView) d.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
@@ -112,7 +100,7 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
} else {
|
} else {
|
||||||
result = Html.fromHtml(getString(R.string.app_developers_));
|
result = Html.fromHtml(getString(R.string.app_developers_));
|
||||||
}
|
}
|
||||||
AlertDialog d = builder
|
AlertDialog d = new AlertDialogBuilder(this)
|
||||||
.setTitle(R.string.app_developers)
|
.setTitle(R.string.app_developers)
|
||||||
.setMessage(result)
|
.setMessage(result)
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
@@ -142,18 +130,4 @@ public class AboutActivity extends AppCompatActivity {
|
|||||||
setFloating();
|
setFloating();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFloating() {
|
|
||||||
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
|
|
||||||
if (isTablet) {
|
|
||||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
|
||||||
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
|
|
||||||
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
|
|
||||||
params.alpha = 1.0f;
|
|
||||||
params.dimAmount = 0.6f;
|
|
||||||
params.flags |= 2;
|
|
||||||
getWindow().setAttributes(params);
|
|
||||||
setFinishOnTouchOutside(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.design.widget.CoordinatorLayout;
|
|
||||||
import android.support.design.widget.Snackbar;
|
|
||||||
import android.support.v4.view.ViewCompat;
|
|
||||||
import android.support.v4.view.ViewPropertyAnimatorCompat;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.github.clans.fab.FloatingActionMenu;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Matteo on 08/08/2015.
|
|
||||||
*
|
|
||||||
* Floating Action Menu Behavior for Clans.FloatingActionButton
|
|
||||||
* https://github.com/Clans/FloatingActionButton/
|
|
||||||
*
|
|
||||||
* Use this behavior as your app:layout_behavior attribute in your Floating Action Menu to use the
|
|
||||||
* FabMenu in a Coordinator Layout.
|
|
||||||
*
|
|
||||||
* Remember to use the correct namespace for the fab:
|
|
||||||
* xmlns:fab="http://schemas.android.com/apk/res-auto"
|
|
||||||
*/
|
|
||||||
public class FABBehavior extends CoordinatorLayout.Behavior {
|
|
||||||
private float mTranslationY;
|
|
||||||
|
|
||||||
public FABBehavior(Context context, AttributeSet attrs) {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
|
|
||||||
return dependency instanceof Snackbar.SnackbarLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
|
|
||||||
if (dependency instanceof Snackbar.SnackbarLayout) {
|
|
||||||
updateTranslation(parent, child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDependentViewRemoved(CoordinatorLayout parent, View child, View dependency) {
|
|
||||||
if (dependency instanceof Snackbar.SnackbarLayout) {
|
|
||||||
revertTranslation(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTranslation(CoordinatorLayout parent, View child) {
|
|
||||||
float translationY = getTranslationY(parent, child);
|
|
||||||
if (translationY != mTranslationY) {
|
|
||||||
ViewPropertyAnimatorCompat anim = ViewCompat.animate(child);
|
|
||||||
anim.cancel();
|
|
||||||
anim.translationY(translationY).setDuration(100);
|
|
||||||
mTranslationY = translationY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void revertTranslation(View child) {
|
|
||||||
if (mTranslationY != 0) {
|
|
||||||
ViewPropertyAnimatorCompat anim = ViewCompat.animate(child);
|
|
||||||
anim.cancel();
|
|
||||||
anim.translationY(0).setDuration(100);
|
|
||||||
mTranslationY = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float getTranslationY(CoordinatorLayout parent, View child) {
|
|
||||||
float minOffset = 0.0F;
|
|
||||||
List dependencies = parent.getDependencies(child);
|
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
for (int z = dependencies.size(); i < z; ++i) {
|
|
||||||
View view = (View) dependencies.get(i);
|
|
||||||
if (view instanceof Snackbar.SnackbarLayout && parent.doViewsOverlap(child, view)) {
|
|
||||||
minOffset = Math.min(minOffset, ViewCompat.getTranslationY(view) - (float) view.getHeight());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return minOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onStartNestedScroll and onNestedScroll will hide/show the FabMenu when a scroll is detected.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
|
|
||||||
View directTargetChild, View target, int nestedScrollAxes) {
|
|
||||||
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL ||
|
|
||||||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target,
|
|
||||||
nestedScrollAxes);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
|
|
||||||
int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
|
|
||||||
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed,
|
|
||||||
dyUnconsumed);
|
|
||||||
FloatingActionMenu fabMenu = (FloatingActionMenu) child;
|
|
||||||
if (dyConsumed > 0 && !fabMenu.isMenuButtonHidden()) {
|
|
||||||
fabMenu.hideMenuButton(true);
|
|
||||||
} else if (dyConsumed < 0 && fabMenu.isMenuButtonHidden()) {
|
|
||||||
fabMenu.showMenuButton(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v7.widget.CardView;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.receivers.MagiskDlReceiver;
|
|
||||||
import com.topjohnwu.magisk.utils.CallbackHandler;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
|
|
||||||
public class InstallFragment extends Fragment implements CallbackHandler.EventListener {
|
|
||||||
|
|
||||||
public static final CallbackHandler.Event blockDetectionDone = new CallbackHandler.Event();
|
|
||||||
|
|
||||||
public static List<String> blockList;
|
|
||||||
public static String bootBlock = null;
|
|
||||||
|
|
||||||
@BindView(R.id.install_title) TextView installTitle;
|
|
||||||
@BindView(R.id.block_spinner) Spinner spinner;
|
|
||||||
@BindView(R.id.detect_bootimage) Button detectButton;
|
|
||||||
@BindView(R.id.flash_button) CardView flashButton;
|
|
||||||
@BindView(R.id.keep_force_enc) CheckBox keepEncChkbox;
|
|
||||||
@BindView(R.id.keep_verity) CheckBox keepVerityChkbox;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View v = inflater.inflate(R.layout.install_fragment, container, false);
|
|
||||||
ButterKnife.bind(this, v);
|
|
||||||
detectButton.setOnClickListener(v1 -> toAutoDetect());
|
|
||||||
installTitle.setText(getString(R.string.install_magisk_title, StatusFragment.remoteMagiskVersion));
|
|
||||||
flashButton.setOnClickListener(v1 -> {
|
|
||||||
String bootImage = bootBlock;
|
|
||||||
if (bootImage == null) {
|
|
||||||
bootImage = blockList.get(spinner.getSelectedItemPosition() - 1);
|
|
||||||
}
|
|
||||||
String filename = "Magisk-v" + String.valueOf(StatusFragment.remoteMagiskVersion) + ".zip";
|
|
||||||
String finalBootImage = bootImage;
|
|
||||||
MainActivity.alertBuilder
|
|
||||||
.setTitle(getString(R.string.repo_install_title, getString(R.string.magisk)))
|
|
||||||
.setMessage(getString(R.string.repo_install_msg, filename))
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(R.string.download_install, (dialogInterface, i) -> Utils.dlAndReceive(
|
|
||||||
getActivity(),
|
|
||||||
new MagiskDlReceiver(finalBootImage, keepEncChkbox.isChecked(), keepVerityChkbox.isChecked()),
|
|
||||||
StatusFragment.magiskLink,
|
|
||||||
Utils.getLegalFilename(filename)))
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
});
|
|
||||||
if (blockDetectionDone.isTriggered) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrigger(CallbackHandler.Event event) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
List<String> items = new ArrayList<>(blockList);
|
|
||||||
items.add(0, getString(R.string.auto_detect, bootBlock));
|
|
||||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
|
|
||||||
android.R.layout.simple_spinner_item, items);
|
|
||||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
spinner.setAdapter(adapter);
|
|
||||||
toAutoDetect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void toAutoDetect() {
|
|
||||||
if (bootBlock != null) {
|
|
||||||
spinner.setSelection(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
getActivity().setTitle(R.string.install);
|
|
||||||
CallbackHandler.register(blockDetectionDone, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
CallbackHandler.unRegister(blockDetectionDone, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,236 +1,53 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Environment;
|
import android.support.design.widget.TabLayout;
|
||||||
import android.os.Handler;
|
import android.support.v4.view.ViewPager;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.design.widget.Snackbar;
|
|
||||||
import android.support.v4.app.ActivityCompat;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.HorizontalScrollView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.adapters.TabFragmentAdapter;
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
public class LogFragment extends Fragment {
|
public class LogFragment extends Fragment {
|
||||||
|
|
||||||
private static final String MAGISK_LOG = "/cache/magisk.log";
|
private Unbinder unbinder;
|
||||||
|
|
||||||
@BindView(R.id.txtLog) TextView txtLog;
|
@BindView(R.id.container) ViewPager viewPager;
|
||||||
@BindView(R.id.svLog) ScrollView svLog;
|
@BindView(R.id.tab) TabLayout tab;
|
||||||
@BindView(R.id.hsvLog) HorizontalScrollView hsvLog;
|
|
||||||
|
|
||||||
@BindView(R.id.progressBar) ProgressBar progressBar;
|
|
||||||
|
|
||||||
private MenuItem mClickedMenuItem;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
View view = inflater.inflate(R.layout.log_fragment, container, false);
|
Bundle savedInstanceState) {
|
||||||
ButterKnife.bind(this, view);
|
// Inflate the layout for this fragment
|
||||||
|
View v = inflater.inflate(R.layout.fragment_log, container, false);
|
||||||
|
unbinder = ButterKnife.bind(this, v);
|
||||||
|
|
||||||
txtLog.setTextIsSelectable(true);
|
TabFragmentAdapter adapter = new TabFragmentAdapter(getChildFragmentManager());
|
||||||
|
|
||||||
new LogManager().read();
|
if (getApplication().isSuClient) {
|
||||||
|
adapter.addTab(new SuLogFragment(), getString(R.string.superuser));
|
||||||
|
tab.setupWithViewPager(viewPager);
|
||||||
|
tab.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
return view;
|
adapter.addTab(new MagiskLogFragment(), getString(R.string.magisk));
|
||||||
|
|
||||||
|
viewPager.setAdapter(adapter);
|
||||||
|
|
||||||
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onDestroyView() {
|
||||||
super.onResume();
|
super.onDestroyView();
|
||||||
setHasOptionsMenu(true);
|
unbinder.unbind();
|
||||||
new LogManager().read();
|
|
||||||
getActivity().setTitle(R.string.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.menu_log, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
mClickedMenuItem = item;
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_refresh:
|
|
||||||
new LogManager().read();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_send:
|
|
||||||
new LogManager().send();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_save:
|
|
||||||
new LogManager().save();
|
|
||||||
return true;
|
|
||||||
case R.id.menu_clear:
|
|
||||||
new LogManager().clear();
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
|
||||||
if (requestCode == 0) {
|
|
||||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
if (mClickedMenuItem != null) {
|
|
||||||
new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Snackbar.make(txtLog, R.string.permissionNotGranted, Snackbar.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LogManager extends Async.RootTask<Object, Void, Object> {
|
|
||||||
|
|
||||||
int mode;
|
|
||||||
File targetFile;
|
|
||||||
|
|
||||||
@SuppressLint("DefaultLocale")
|
|
||||||
@Override
|
|
||||||
protected Object doInBackground(Object... params) {
|
|
||||||
mode = (int) params[0];
|
|
||||||
switch (mode) {
|
|
||||||
case 0:
|
|
||||||
List<String> logList = Utils.readFile(MAGISK_LOG);
|
|
||||||
|
|
||||||
StringBuilder llog = new StringBuilder(15 * 10 * 1024);
|
|
||||||
for (String s : logList) {
|
|
||||||
llog.append(s).append("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
return llog.toString();
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
Shell.su("echo > " + MAGISK_LOG);
|
|
||||||
Snackbar.make(txtLog, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
|
|
||||||
return "";
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
Calendar now = Calendar.getInstance();
|
|
||||||
String filename = String.format(
|
|
||||||
"magisk_%s_%04d%02d%02d_%02d%02d%02d.log", "error",
|
|
||||||
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
|
||||||
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
|
||||||
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
|
|
||||||
|
|
||||||
targetFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MagiskManager/" + filename);
|
|
||||||
|
|
||||||
if ((!targetFile.getParentFile().exists() && !targetFile.getParentFile().mkdirs())
|
|
||||||
|| (targetFile.exists() && !targetFile.delete()))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
List<String> in = Utils.readFile(MAGISK_LOG);
|
|
||||||
|
|
||||||
try {
|
|
||||||
FileWriter out = new FileWriter(targetFile);
|
|
||||||
for (String line : in) {
|
|
||||||
out.write(line + "\n");
|
|
||||||
}
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Object o) {
|
|
||||||
boolean bool;
|
|
||||||
String llog;
|
|
||||||
switch (mode) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
llog = (String) o;
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
if (llog.length() == 0)
|
|
||||||
txtLog.setText(R.string.log_is_empty);
|
|
||||||
else
|
|
||||||
txtLog.setText(llog);
|
|
||||||
svLog.post(() -> svLog.scrollTo(0, txtLog.getHeight()));
|
|
||||||
hsvLog.post(() -> hsvLog.scrollTo(0, 0));
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
bool = (boolean) o;
|
|
||||||
if (bool)
|
|
||||||
Toast.makeText(getActivity(), targetFile.toString(), Toast.LENGTH_LONG).show();
|
|
||||||
else
|
|
||||||
Toast.makeText(getActivity(), getString(R.string.logs_save_failed), Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
bool = (boolean) o;
|
|
||||||
if (bool) {
|
|
||||||
Intent sendIntent = new Intent();
|
|
||||||
sendIntent.setAction(Intent.ACTION_SEND);
|
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(targetFile));
|
|
||||||
sendIntent.setType("application/html");
|
|
||||||
startActivity(Intent.createChooser(sendIntent, getResources().getString(R.string.menuSend)));
|
|
||||||
} else {
|
|
||||||
Toast.makeText(getActivity(), getString(R.string.logs_save_failed), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void read() {
|
|
||||||
exec(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
exec(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
exec(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void send() {
|
|
||||||
exec(3);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
509
app/src/main/java/com/topjohnwu/magisk/MagiskFragment.java
Normal file
509
app/src/main/java/com/topjohnwu/magisk/MagiskFragment.java
Normal file
@@ -0,0 +1,509 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.CountDownTimer;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
|
import android.support.v7.widget.CardView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.asyncs.CheckUpdates;
|
||||||
|
import com.topjohnwu.magisk.asyncs.ProcessMagiskZip;
|
||||||
|
import com.topjohnwu.magisk.components.AlertDialogBuilder;
|
||||||
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
|
import com.topjohnwu.magisk.components.SnackbarMaker;
|
||||||
|
import com.topjohnwu.magisk.receivers.DownloadReceiver;
|
||||||
|
import com.topjohnwu.magisk.utils.CallbackEvent;
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import butterknife.BindColor;
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.OnClick;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
|
public class MagiskFragment extends Fragment
|
||||||
|
implements CallbackEvent.Listener<Void>, SwipeRefreshLayout.OnRefreshListener {
|
||||||
|
|
||||||
|
private static boolean noDialog = false;
|
||||||
|
private static int expandHeight = 0;
|
||||||
|
private static boolean mExpanded = false;
|
||||||
|
|
||||||
|
private MagiskManager magiskManager;
|
||||||
|
private Unbinder unbinder;
|
||||||
|
|
||||||
|
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
||||||
|
|
||||||
|
@BindView(R.id.magisk_update_card) CardView magiskUpdateCard;
|
||||||
|
@BindView(R.id.magisk_update_icon) ImageView magiskUpdateIcon;
|
||||||
|
@BindView(R.id.magisk_update_status) TextView magiskUpdateText;
|
||||||
|
@BindView(R.id.magisk_update_progress) ProgressBar magiskUpdateProgress;
|
||||||
|
|
||||||
|
@BindView(R.id.magisk_status_icon) ImageView magiskStatusIcon;
|
||||||
|
@BindView(R.id.magisk_version) TextView magiskVersionText;
|
||||||
|
@BindView(R.id.root_status_icon) ImageView rootStatusIcon;
|
||||||
|
@BindView(R.id.root_status) TextView rootStatusText;
|
||||||
|
|
||||||
|
@BindView(R.id.safetyNet_card) CardView safetyNetCard;
|
||||||
|
@BindView(R.id.safetyNet_refresh) ImageView safetyNetRefreshIcon;
|
||||||
|
@BindView(R.id.safetyNet_status) TextView safetyNetStatusText;
|
||||||
|
@BindView(R.id.safetyNet_check_progress) ProgressBar safetyNetProgress;
|
||||||
|
@BindView(R.id.expand_layout) LinearLayout expandLayout;
|
||||||
|
@BindView(R.id.cts_status_icon) ImageView ctsStatusIcon;
|
||||||
|
@BindView(R.id.cts_status) TextView ctsStatusText;
|
||||||
|
@BindView(R.id.basic_status_icon) ImageView basicStatusIcon;
|
||||||
|
@BindView(R.id.basic_status) TextView basicStatusText;
|
||||||
|
|
||||||
|
@BindView(R.id.bootimage_card) CardView bootImageCard;
|
||||||
|
@BindView(R.id.block_spinner) Spinner spinner;
|
||||||
|
@BindView(R.id.detect_bootimage) Button detectButton;
|
||||||
|
@BindView(R.id.install_option_card) CardView installOptionCard;
|
||||||
|
@BindView(R.id.keep_force_enc) CheckBox keepEncChkbox;
|
||||||
|
@BindView(R.id.keep_verity) CheckBox keepVerityChkbox;
|
||||||
|
@BindView(R.id.install_button) CardView installButton;
|
||||||
|
@BindView(R.id.install_text) TextView installText;
|
||||||
|
@BindView(R.id.uninstall_button) CardView uninstallButton;
|
||||||
|
|
||||||
|
@BindColor(R.color.red500) int colorBad;
|
||||||
|
@BindColor(R.color.green500) int colorOK;
|
||||||
|
@BindColor(R.color.yellow500) int colorWarn;
|
||||||
|
@BindColor(R.color.grey500) int colorNeutral;
|
||||||
|
@BindColor(R.color.blue500) int colorInfo;
|
||||||
|
|
||||||
|
@OnClick(R.id.safetyNet_title)
|
||||||
|
public void safetyNet() {
|
||||||
|
safetyNetProgress.setVisibility(View.VISIBLE);
|
||||||
|
safetyNetRefreshIcon.setVisibility(View.GONE);
|
||||||
|
safetyNetStatusText.setText(R.string.checking_safetyNet_status);
|
||||||
|
Utils.checkSafetyNet(getActivity());
|
||||||
|
collapse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.detect_bootimage)
|
||||||
|
public void toAutoDetect() {
|
||||||
|
if (magiskManager.bootBlock != null) {
|
||||||
|
spinner.setSelection(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.install_button)
|
||||||
|
public void install() {
|
||||||
|
String bootImage = null;
|
||||||
|
if (magiskManager.blockList != null) {
|
||||||
|
int idx = spinner.getSelectedItemPosition();
|
||||||
|
if (magiskManager.bootBlock != null) {
|
||||||
|
bootImage = magiskManager.bootBlock;
|
||||||
|
} else {
|
||||||
|
if (idx > 0) {
|
||||||
|
bootImage = magiskManager.blockList.get(idx - 1);
|
||||||
|
} else {
|
||||||
|
SnackbarMaker.make(getActivity(), R.string.manual_boot_image, Snackbar.LENGTH_LONG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final String finalBootImage = bootImage;
|
||||||
|
String filename = "Magisk-v" + magiskManager.remoteMagiskVersionString + ".zip";
|
||||||
|
new AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(getString(R.string.repo_install_title, getString(R.string.magisk)))
|
||||||
|
.setMessage(getString(R.string.repo_install_msg, filename))
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(Shell.rootAccess() ? R.string.install : R.string.download,
|
||||||
|
(dialogInterface, i) -> Utils.dlAndReceive(
|
||||||
|
getActivity(),
|
||||||
|
new DownloadReceiver() {
|
||||||
|
private String boot = finalBootImage;
|
||||||
|
private boolean enc = keepEncChkbox.isChecked();
|
||||||
|
private boolean verity = keepVerityChkbox.isChecked();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDownloadDone(Uri uri) {
|
||||||
|
new ProcessMagiskZip(getActivity(), uri, boot, enc, verity).exec();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
magiskManager.magiskLink,
|
||||||
|
Utils.getLegalFilename(filename)))
|
||||||
|
.setNeutralButton(R.string.release_notes, (dialog, which) -> {
|
||||||
|
if (magiskManager.releaseNoteLink != null) {
|
||||||
|
Intent openReleaseNoteLink = new Intent(Intent.ACTION_VIEW, Uri.parse(magiskManager.releaseNoteLink));
|
||||||
|
openReleaseNoteLink.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
magiskManager.startActivity(openReleaseNoteLink);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClick(R.id.uninstall_button)
|
||||||
|
public void uninstall() {
|
||||||
|
new AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.uninstall_magisk_title)
|
||||||
|
.setMessage(R.string.uninstall_magisk_msg)
|
||||||
|
.setPositiveButton(R.string.yes, (dialogInterface, i) -> {
|
||||||
|
try {
|
||||||
|
InputStream in = magiskManager.getAssets().open(MagiskManager.UNINSTALLER);
|
||||||
|
File uninstaller = new File(magiskManager.getCacheDir(), MagiskManager.UNINSTALLER);
|
||||||
|
FileOutputStream out = new FileOutputStream(uninstaller);
|
||||||
|
byte[] bytes = new byte[1024];
|
||||||
|
int read;
|
||||||
|
while ((read = in.read(bytes)) != -1) {
|
||||||
|
out.write(bytes, 0, read);
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
out.close();
|
||||||
|
in = magiskManager.getAssets().open(MagiskManager.UTIL_FUNCTIONS);
|
||||||
|
File utils = new File(magiskManager.getCacheDir(), MagiskManager.UTIL_FUNCTIONS);
|
||||||
|
out = new FileOutputStream(utils);
|
||||||
|
while ((read = in.read(bytes)) != -1) {
|
||||||
|
out.write(bytes, 0, read);
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
out.close();
|
||||||
|
ProgressDialog progress = new ProgressDialog(getActivity());
|
||||||
|
progress.setTitle(R.string.reboot);
|
||||||
|
progress.show();
|
||||||
|
new CountDownTimer(5000, 1000) {
|
||||||
|
@Override
|
||||||
|
public void onTick(long millisUntilFinished) {
|
||||||
|
progress.setMessage(getString(R.string.reboot_countdown, millisUntilFinished / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFinish() {
|
||||||
|
progress.setMessage(getString(R.string.reboot_countdown, 0));
|
||||||
|
Shell.su(true,
|
||||||
|
"mv -f " + uninstaller + " /cache/" + MagiskManager.UNINSTALLER,
|
||||||
|
"mv -f " + utils + " /data/magisk/" + MagiskManager.UTIL_FUNCTIONS,
|
||||||
|
"reboot"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View v = inflater.inflate(R.layout.fragment_magisk, container, false);
|
||||||
|
unbinder = ButterKnife.bind(this, v);
|
||||||
|
magiskManager = getApplication();
|
||||||
|
|
||||||
|
expandLayout.getViewTreeObserver().addOnPreDrawListener(
|
||||||
|
new ViewTreeObserver.OnPreDrawListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreDraw() {
|
||||||
|
if (expandHeight == 0) {
|
||||||
|
final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
expandLayout.measure(widthSpec, heightSpec);
|
||||||
|
expandHeight = expandLayout.getMeasuredHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
|
setExpanded();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
mSwipeRefreshLayout.setOnRefreshListener(this);
|
||||||
|
|
||||||
|
if (magiskManager.magiskVersionCode < 0 && Shell.rootAccess() && !noDialog) {
|
||||||
|
noDialog = true;
|
||||||
|
new AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.no_magisk_title)
|
||||||
|
.setMessage(R.string.no_magisk_msg)
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.goto_install, (d, i) -> {})
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRefresh() {
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
magiskUpdateText.setText(R.string.checking_for_updates);
|
||||||
|
magiskUpdateProgress.setVisibility(View.VISIBLE);
|
||||||
|
magiskUpdateIcon.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
safetyNetStatusText.setText(R.string.safetyNet_check_text);
|
||||||
|
|
||||||
|
magiskManager.safetyNetDone.isTriggered = false;
|
||||||
|
magiskManager.updateCheckDone.isTriggered = false;
|
||||||
|
magiskManager.remoteMagiskVersionString = null;
|
||||||
|
magiskManager.remoteMagiskVersionCode = -1;
|
||||||
|
collapse();
|
||||||
|
noDialog = false;
|
||||||
|
|
||||||
|
// Trigger state check
|
||||||
|
if (Utils.checkNetworkStatus(magiskManager)) {
|
||||||
|
new CheckUpdates(getActivity()).exec();
|
||||||
|
} else {
|
||||||
|
mSwipeRefreshLayout.setRefreshing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTrigger(CallbackEvent<Void> event) {
|
||||||
|
if (event == magiskManager.updateCheckDone) {
|
||||||
|
updateCheckUI();
|
||||||
|
} else if (event == magiskManager.safetyNetDone) {
|
||||||
|
updateSafetyNetUI();
|
||||||
|
} else if (event == magiskManager.blockDetectionDone) {
|
||||||
|
updateInstallUI();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
// Manual trigger if already done
|
||||||
|
if (magiskManager.updateCheckDone.isTriggered)
|
||||||
|
updateCheckUI();
|
||||||
|
if (magiskManager.safetyNetDone.isTriggered)
|
||||||
|
updateSafetyNetUI();
|
||||||
|
if (magiskManager.blockDetectionDone.isTriggered || !Shell.rootAccess())
|
||||||
|
updateInstallUI();
|
||||||
|
magiskManager.updateCheckDone.register(this);
|
||||||
|
magiskManager.safetyNetDone.register(this);
|
||||||
|
magiskManager.blockDetectionDone.register(this);
|
||||||
|
getActivity().setTitle(R.string.magisk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
magiskManager.updateCheckDone.unRegister(this);
|
||||||
|
magiskManager.safetyNetDone.unRegister(this);
|
||||||
|
magiskManager.blockDetectionDone.unRegister(this);
|
||||||
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
unbinder.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI() {
|
||||||
|
((MainActivity) getActivity()).checkHideSection();
|
||||||
|
final int ROOT = 0x1, NETWORK = 0x2, UPTODATE = 0x4;
|
||||||
|
int status = 0;
|
||||||
|
status |= Shell.rootAccess() ? ROOT : 0;
|
||||||
|
status |= Utils.checkNetworkStatus(magiskManager) ? NETWORK : 0;
|
||||||
|
status |= magiskManager.magiskVersionCode >= 130 ? UPTODATE : 0;
|
||||||
|
magiskUpdateCard.setVisibility(Utils.checkBits(status, NETWORK) ? View.VISIBLE : View.GONE);
|
||||||
|
safetyNetCard.setVisibility(Utils.checkBits(status, NETWORK) ? View.VISIBLE : View.GONE);
|
||||||
|
bootImageCard.setVisibility(Utils.checkBits(status, NETWORK, ROOT) ? View.VISIBLE : View.GONE);
|
||||||
|
installOptionCard.setVisibility(Utils.checkBits(status, NETWORK, ROOT) ? View.VISIBLE : View.GONE);
|
||||||
|
installButton.setVisibility(Utils.checkBits(status, NETWORK) ? View.VISIBLE : View.GONE);
|
||||||
|
uninstallButton.setVisibility(Utils.checkBits(status, UPTODATE, ROOT) ? View.VISIBLE : View.GONE);
|
||||||
|
updateVersionUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateVersionUI() {
|
||||||
|
int image, color;
|
||||||
|
|
||||||
|
magiskManager.updateMagiskInfo();
|
||||||
|
|
||||||
|
if (magiskManager.magiskVersionCode < 0) {
|
||||||
|
color = colorBad;
|
||||||
|
image = R.drawable.ic_cancel;
|
||||||
|
magiskVersionText.setText(R.string.magisk_version_error);
|
||||||
|
} else {
|
||||||
|
color = colorOK;
|
||||||
|
image = R.drawable.ic_check_circle;
|
||||||
|
magiskVersionText.setText(getString(R.string.current_magisk_title, "v" + magiskManager.magiskVersionString));
|
||||||
|
}
|
||||||
|
|
||||||
|
magiskStatusIcon.setImageResource(image);
|
||||||
|
magiskStatusIcon.setColorFilter(color);
|
||||||
|
|
||||||
|
switch (Shell.rootStatus) {
|
||||||
|
case 0:
|
||||||
|
color = colorBad;
|
||||||
|
image = R.drawable.ic_cancel;
|
||||||
|
rootStatusText.setText(R.string.not_rooted);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
if (magiskManager.suVersion != null) {
|
||||||
|
color = colorOK;
|
||||||
|
image = R.drawable.ic_check_circle;
|
||||||
|
rootStatusText.setText(magiskManager.suVersion);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case -1:
|
||||||
|
default:
|
||||||
|
color = colorNeutral;
|
||||||
|
image = R.drawable.ic_help;
|
||||||
|
rootStatusText.setText(R.string.root_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
rootStatusIcon.setImageResource(image);
|
||||||
|
rootStatusIcon.setColorFilter(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCheckUI() {
|
||||||
|
int image, color;
|
||||||
|
|
||||||
|
if (magiskManager.remoteMagiskVersionCode < 0) {
|
||||||
|
color = colorNeutral;
|
||||||
|
image = R.drawable.ic_help;
|
||||||
|
magiskUpdateText.setText(R.string.cannot_check_updates);
|
||||||
|
} else {
|
||||||
|
color = colorOK;
|
||||||
|
image = R.drawable.ic_check_circle;
|
||||||
|
magiskUpdateText.setText(getString(R.string.install_magisk_title, "v" + magiskManager.remoteMagiskVersionString));
|
||||||
|
}
|
||||||
|
|
||||||
|
magiskUpdateIcon.setImageResource(image);
|
||||||
|
magiskUpdateIcon.setColorFilter(color);
|
||||||
|
magiskUpdateIcon.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
magiskUpdateProgress.setVisibility(View.GONE);
|
||||||
|
mSwipeRefreshLayout.setRefreshing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateInstallUI() {
|
||||||
|
if (!Shell.rootAccess()) {
|
||||||
|
installText.setText(R.string.download);
|
||||||
|
} else {
|
||||||
|
installText.setText(R.string.download_install);
|
||||||
|
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
|
if (magiskManager.bootBlock != null) {
|
||||||
|
items.add(getString(R.string.auto_detect, magiskManager.bootBlock));
|
||||||
|
spinner.setEnabled(false);
|
||||||
|
} else {
|
||||||
|
items.add(getString(R.string.cannot_auto_detect));
|
||||||
|
items.addAll(magiskManager.blockList);
|
||||||
|
}
|
||||||
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(),
|
||||||
|
android.R.layout.simple_spinner_item, items);
|
||||||
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
|
spinner.setAdapter(adapter);
|
||||||
|
toAutoDetect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSafetyNetUI() {
|
||||||
|
int image, color;
|
||||||
|
safetyNetProgress.setVisibility(View.GONE);
|
||||||
|
safetyNetRefreshIcon.setVisibility(View.VISIBLE);
|
||||||
|
if (magiskManager.SNCheckResult.failed) {
|
||||||
|
safetyNetStatusText.setText(magiskManager.SNCheckResult.errmsg);
|
||||||
|
collapse();
|
||||||
|
} else {
|
||||||
|
safetyNetStatusText.setText(R.string.safetyNet_check_success);
|
||||||
|
if (magiskManager.SNCheckResult.ctsProfile) {
|
||||||
|
color = colorOK;
|
||||||
|
image = R.drawable.ic_check_circle;
|
||||||
|
} else {
|
||||||
|
color = colorBad;
|
||||||
|
image = R.drawable.ic_cancel;
|
||||||
|
}
|
||||||
|
ctsStatusText.setText("ctsProfile: " + magiskManager.SNCheckResult.ctsProfile);
|
||||||
|
ctsStatusIcon.setImageResource(image);
|
||||||
|
ctsStatusIcon.setColorFilter(color);
|
||||||
|
|
||||||
|
if (magiskManager.SNCheckResult.basicIntegrity) {
|
||||||
|
color = colorOK;
|
||||||
|
image = R.drawable.ic_check_circle;
|
||||||
|
} else {
|
||||||
|
color = colorBad;
|
||||||
|
image = R.drawable.ic_cancel;
|
||||||
|
}
|
||||||
|
basicStatusText.setText("basicIntegrity: " + magiskManager.SNCheckResult.basicIntegrity);
|
||||||
|
basicStatusIcon.setImageResource(image);
|
||||||
|
basicStatusIcon.setColorFilter(color);
|
||||||
|
expand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setExpanded() {
|
||||||
|
ViewGroup.LayoutParams layoutParams = expandLayout.getLayoutParams();
|
||||||
|
layoutParams.height = mExpanded ? expandHeight : 0;
|
||||||
|
expandLayout.setLayoutParams(layoutParams);
|
||||||
|
expandLayout.setVisibility(mExpanded ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expand() {
|
||||||
|
if (mExpanded) return;
|
||||||
|
expandLayout.setVisibility(View.VISIBLE);
|
||||||
|
ValueAnimator mAnimator = slideAnimator(0, expandHeight);
|
||||||
|
mAnimator.start();
|
||||||
|
mExpanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collapse() {
|
||||||
|
if (!mExpanded) return;
|
||||||
|
int finalHeight = expandLayout.getHeight();
|
||||||
|
ValueAnimator mAnimator = slideAnimator(finalHeight, 0);
|
||||||
|
mAnimator.addListener(new Animator.AnimatorListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animator) {
|
||||||
|
expandLayout.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animator) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(Animator animator) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animator animator) {}
|
||||||
|
});
|
||||||
|
mAnimator.start();
|
||||||
|
mExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ValueAnimator slideAnimator(int start, int end) {
|
||||||
|
|
||||||
|
ValueAnimator animator = ValueAnimator.ofInt(start, end);
|
||||||
|
|
||||||
|
animator.addUpdateListener(valueAnimator -> {
|
||||||
|
int value = (Integer) valueAnimator.getAnimatedValue();
|
||||||
|
ViewGroup.LayoutParams layoutParams = expandLayout.getLayoutParams();
|
||||||
|
layoutParams.height = value;
|
||||||
|
expandLayout.setLayoutParams(layoutParams);
|
||||||
|
});
|
||||||
|
return animator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@@ -16,60 +15,67 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.SearchView;
|
import android.widget.SearchView;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.adapters.ApplicationAdapter;
|
import com.topjohnwu.magisk.adapters.ApplicationAdapter;
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.asyncs.MagiskHide;
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
|
import com.topjohnwu.magisk.utils.CallbackEvent;
|
||||||
import java.util.ArrayList;
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
public class MagiskHideFragment extends Fragment {
|
public class MagiskHideFragment extends Fragment implements CallbackEvent.Listener<Void> {
|
||||||
|
|
||||||
|
private Unbinder unbinder;
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
||||||
|
|
||||||
private PackageManager packageManager;
|
private ApplicationAdapter appAdapter;
|
||||||
private View mView;
|
|
||||||
private List<ApplicationInfo> listApps = new ArrayList<>(), fListApps = new ArrayList<>();
|
|
||||||
private List<String> hideList = new ArrayList<>();
|
|
||||||
private ApplicationAdapter appAdapter = new ApplicationAdapter(fListApps, hideList);
|
|
||||||
|
|
||||||
private SearchView.OnQueryTextListener searchListener;
|
private SearchView.OnQueryTextListener searchListener;
|
||||||
|
private String lastFilter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
mView = inflater.inflate(R.layout.magisk_hide_fragment, container, false);
|
View view = inflater.inflate(R.layout.fragment_magisk_hide, container, false);
|
||||||
ButterKnife.bind(this, mView);
|
unbinder = ButterKnife.bind(this, view);
|
||||||
|
|
||||||
packageManager = getActivity().getPackageManager();
|
PackageManager packageManager = getActivity().getPackageManager();
|
||||||
|
|
||||||
mSwipeRefreshLayout.setRefreshing(true);
|
mSwipeRefreshLayout.setRefreshing(true);
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
mSwipeRefreshLayout.setOnRefreshListener(() -> new MagiskHide(getActivity()).list());
|
||||||
recyclerView.setVisibility(View.GONE);
|
|
||||||
new LoadApps().exec();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
appAdapter = new ApplicationAdapter(packageManager);
|
||||||
recyclerView.setAdapter(appAdapter);
|
recyclerView.setAdapter(appAdapter);
|
||||||
|
|
||||||
searchListener = new SearchView.OnQueryTextListener() {
|
searchListener = new SearchView.OnQueryTextListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onQueryTextSubmit(String query) {
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
lastFilter = query;
|
||||||
|
appAdapter.filter(query);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onQueryTextChange(String newText) {
|
public boolean onQueryTextChange(String newText) {
|
||||||
new FilterApps().exec(newText);
|
lastFilter = newText;
|
||||||
|
appAdapter.filter(newText);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
new LoadApps().exec();
|
if (getApplication().magiskHideDone.isTriggered) {
|
||||||
return mView;
|
onTrigger(getApplication().magiskHideDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -80,57 +86,31 @@ public class MagiskHideFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onStart() {
|
||||||
super.onResume();
|
super.onStart();
|
||||||
setHasOptionsMenu(true);
|
|
||||||
mView = this.getView();
|
|
||||||
getActivity().setTitle(R.string.magiskhide);
|
getActivity().setTitle(R.string.magiskhide);
|
||||||
|
getApplication().magiskHideDone.register(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class LoadApps extends Async.RootTask<Void, Void, Void> {
|
@Override
|
||||||
@Override
|
public void onStop() {
|
||||||
protected Void doInBackground(Void... voids) {
|
getApplication().magiskHideDone.unRegister(this);
|
||||||
listApps.clear();
|
super.onStop();
|
||||||
hideList.clear();
|
|
||||||
fListApps.clear();
|
|
||||||
listApps.addAll(packageManager.getInstalledApplications(PackageManager.GET_META_DATA));
|
|
||||||
Collections.sort(listApps, (a, b) -> a.loadLabel(packageManager).toString().toLowerCase()
|
|
||||||
.compareTo(b.loadLabel(packageManager).toString().toLowerCase()));
|
|
||||||
hideList.addAll(Shell.su(Async.MAGISK_HIDE_PATH + "list"));
|
|
||||||
fListApps.addAll(listApps);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid) {
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FilterApps extends Async.NormalTask<String, Void, Void> {
|
@Override
|
||||||
@Override
|
public void onDestroyView() {
|
||||||
protected Void doInBackground(String... strings) {
|
super.onDestroyView();
|
||||||
String newText = strings[0];
|
unbinder.unbind();
|
||||||
fListApps.clear();
|
|
||||||
for (ApplicationInfo info : listApps) {
|
|
||||||
if (info.loadLabel(packageManager).toString().toLowerCase().contains(newText.toLowerCase())
|
|
||||||
|| info.packageName.toLowerCase().contains(newText.toLowerCase())) {
|
|
||||||
fListApps.add(info);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
appAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUI() {
|
@Override
|
||||||
appAdapter.notifyDataSetChanged();
|
public void onTrigger(CallbackEvent<Void> event) {
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
Logger.dev("MagiskHideFragment: UI refresh");
|
||||||
|
appAdapter.setLists(getApplication().appList, getApplication().magiskHideList);
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
mSwipeRefreshLayout.setRefreshing(false);
|
||||||
|
if (!TextUtils.isEmpty(lastFilter)) {
|
||||||
|
appAdapter.filter(lastFilter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
241
app/src/main/java/com/topjohnwu/magisk/MagiskLogFragment.java
Normal file
241
app/src/main/java/com/topjohnwu/magisk/MagiskLogFragment.java
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v4.app.ActivityCompat;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.asyncs.RootTask;
|
||||||
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
|
import com.topjohnwu.magisk.components.SnackbarMaker;
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
|
public class MagiskLogFragment extends Fragment {
|
||||||
|
|
||||||
|
private static final String MAGISK_LOG = "/cache/magisk.log";
|
||||||
|
|
||||||
|
private Unbinder unbinder;
|
||||||
|
@BindView(R.id.txtLog) TextView txtLog;
|
||||||
|
@BindView(R.id.svLog) ScrollView svLog;
|
||||||
|
@BindView(R.id.hsvLog) HorizontalScrollView hsvLog;
|
||||||
|
|
||||||
|
@BindView(R.id.progressBar) ProgressBar progressBar;
|
||||||
|
|
||||||
|
private MenuItem mClickedMenuItem;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.fragment_magisk_log, container, false);
|
||||||
|
unbinder = ButterKnife.bind(this, view);
|
||||||
|
|
||||||
|
txtLog.setTextIsSelectable(true);
|
||||||
|
|
||||||
|
new LogManager().read();
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
getActivity().setTitle(R.string.log);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
new LogManager().read();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
unbinder.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.menu_log, menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
mClickedMenuItem = item;
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.menu_refresh:
|
||||||
|
new LogManager().read();
|
||||||
|
return true;
|
||||||
|
case R.id.menu_save:
|
||||||
|
new LogManager().save();
|
||||||
|
return true;
|
||||||
|
case R.id.menu_clear:
|
||||||
|
new LogManager().clear();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
if (requestCode == 0) {
|
||||||
|
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (mClickedMenuItem != null) {
|
||||||
|
new Handler().postDelayed(() -> onOptionsItemSelected(mClickedMenuItem), 500);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
SnackbarMaker.make(txtLog, R.string.permissionNotGranted, Snackbar.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LogManager extends RootTask<Object, Void, Object> {
|
||||||
|
|
||||||
|
int mode;
|
||||||
|
File targetFile;
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
@Override
|
||||||
|
protected Object doInRoot(Object... params) {
|
||||||
|
mode = (int) params[0];
|
||||||
|
switch (mode) {
|
||||||
|
case 0:
|
||||||
|
List<String> logList = Utils.readFile(MAGISK_LOG);
|
||||||
|
|
||||||
|
if (Utils.isValidShellResponse(logList)) {
|
||||||
|
StringBuilder llog = new StringBuilder(15 * 10 * 1024);
|
||||||
|
for (String s : logList) {
|
||||||
|
llog.append(s).append("\n");
|
||||||
|
}
|
||||||
|
return llog.toString();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
Shell.su("echo > " + MAGISK_LOG);
|
||||||
|
SnackbarMaker.make(txtLog, R.string.logs_cleared, Snackbar.LENGTH_SHORT).show();
|
||||||
|
return "";
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
if (ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Calendar now = Calendar.getInstance();
|
||||||
|
String filename = String.format(
|
||||||
|
"magisk_%s_%04d%02d%02d_%02d%02d%02d.log", "error",
|
||||||
|
now.get(Calendar.YEAR), now.get(Calendar.MONTH) + 1,
|
||||||
|
now.get(Calendar.DAY_OF_MONTH), now.get(Calendar.HOUR_OF_DAY),
|
||||||
|
now.get(Calendar.MINUTE), now.get(Calendar.SECOND));
|
||||||
|
|
||||||
|
targetFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/MagiskManager/" + filename);
|
||||||
|
|
||||||
|
if ((!targetFile.getParentFile().exists() && !targetFile.getParentFile().mkdirs())
|
||||||
|
|| (targetFile.exists() && !targetFile.delete())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> in = Utils.readFile(MAGISK_LOG);
|
||||||
|
|
||||||
|
if (Utils.isValidShellResponse(in)) {
|
||||||
|
try (FileWriter out = new FileWriter(targetFile)) {
|
||||||
|
for (String line : in)
|
||||||
|
out.write(line + "\n");
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Object o) {
|
||||||
|
if (o == null) return;
|
||||||
|
boolean bool;
|
||||||
|
String llog;
|
||||||
|
switch (mode) {
|
||||||
|
case 0:
|
||||||
|
case 1:
|
||||||
|
llog = (String) o;
|
||||||
|
progressBar.setVisibility(View.GONE);
|
||||||
|
if (TextUtils.isEmpty(llog))
|
||||||
|
txtLog.setText(R.string.log_is_empty);
|
||||||
|
else
|
||||||
|
txtLog.setText(llog);
|
||||||
|
svLog.post(() -> svLog.scrollTo(0, txtLog.getHeight()));
|
||||||
|
hsvLog.post(() -> hsvLog.scrollTo(0, 0));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
bool = (boolean) o;
|
||||||
|
if (bool) {
|
||||||
|
Toast.makeText(getActivity(), targetFile.toString(), Toast.LENGTH_LONG).show();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getActivity(), getString(R.string.logs_save_failed), Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void read() {
|
||||||
|
exec(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
exec(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save() {
|
||||||
|
exec(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
220
app/src/main/java/com/topjohnwu/magisk/MagiskManager.java
Normal file
220
app/src/main/java/com/topjohnwu/magisk/MagiskManager.java
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.app.Application;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.database.SuDatabaseHelper;
|
||||||
|
import com.topjohnwu.magisk.module.Module;
|
||||||
|
import com.topjohnwu.magisk.module.Repo;
|
||||||
|
import com.topjohnwu.magisk.utils.CallbackEvent;
|
||||||
|
import com.topjohnwu.magisk.utils.SafetyNetHelper;
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
import com.topjohnwu.magisk.utils.ValueSortedMap;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MagiskManager extends Application {
|
||||||
|
|
||||||
|
public static final String MAGISK_DISABLE_FILE = "/cache/.disable_magisk";
|
||||||
|
public static final String TMP_FOLDER_PATH = "/dev/tmp";
|
||||||
|
public static final String MAGISK_PATH = "/magisk";
|
||||||
|
public static final String UNINSTALLER = "magisk_uninstaller.sh";
|
||||||
|
public static final String UTIL_FUNCTIONS= "util_functions.sh";
|
||||||
|
public static final String INTENT_SECTION = "section";
|
||||||
|
public static final String BUSYBOX_VERSION = "1.26.2";
|
||||||
|
public static final String MAGISKHIDE_PROP = "persist.magisk.hide";
|
||||||
|
public static final String DISABLE_INDICATION_PROP = "ro.magisk.disable";
|
||||||
|
public static final String NOTIFICATION_CHANNEL = "magisk_update_notice";
|
||||||
|
|
||||||
|
// Events
|
||||||
|
public final CallbackEvent<Void> blockDetectionDone = new CallbackEvent<>();
|
||||||
|
public final CallbackEvent<Void> magiskHideDone = new CallbackEvent<>();
|
||||||
|
public final CallbackEvent<Void> reloadMainActivity = new CallbackEvent<>();
|
||||||
|
public final CallbackEvent<Void> moduleLoadDone = new CallbackEvent<>();
|
||||||
|
public final CallbackEvent<Void> repoLoadDone = new CallbackEvent<>();
|
||||||
|
public final CallbackEvent<Void> updateCheckDone = new CallbackEvent<>();
|
||||||
|
public final CallbackEvent<Void> safetyNetDone = new CallbackEvent<>();
|
||||||
|
|
||||||
|
// Info
|
||||||
|
public String magiskVersionString;
|
||||||
|
public int magiskVersionCode = -1;
|
||||||
|
public String remoteMagiskVersionString;
|
||||||
|
public int remoteMagiskVersionCode = -1;
|
||||||
|
public String magiskLink;
|
||||||
|
public String releaseNoteLink;
|
||||||
|
public String remoteManagerVersionString;
|
||||||
|
public int remoteManagerVersionCode = -1;
|
||||||
|
public String managerLink;
|
||||||
|
public SafetyNetHelper.Result SNCheckResult;
|
||||||
|
public String bootBlock = null;
|
||||||
|
public boolean isSuClient = false;
|
||||||
|
public String suVersion = null;
|
||||||
|
public boolean disabled;
|
||||||
|
|
||||||
|
// Data
|
||||||
|
public ValueSortedMap<String, Repo> repoMap;
|
||||||
|
public ValueSortedMap<String, Module> moduleMap;
|
||||||
|
public List<String> blockList;
|
||||||
|
public List<ApplicationInfo> appList;
|
||||||
|
public List<String> magiskHideList;
|
||||||
|
|
||||||
|
// Configurations
|
||||||
|
public static boolean shellLogging;
|
||||||
|
public static boolean devLogging;
|
||||||
|
|
||||||
|
public boolean magiskHide;
|
||||||
|
public boolean isDarkTheme;
|
||||||
|
public boolean updateNotification;
|
||||||
|
public boolean suReauth;
|
||||||
|
public int suRequestTimeout;
|
||||||
|
public int suLogTimeout = 14;
|
||||||
|
public int suAccessState;
|
||||||
|
public int multiuserMode;
|
||||||
|
public int suResponseType;
|
||||||
|
public int suNotificationType;
|
||||||
|
public int suNamespaceMode;
|
||||||
|
|
||||||
|
// Global resources
|
||||||
|
public SharedPreferences prefs;
|
||||||
|
public SuDatabaseHelper suDB;
|
||||||
|
|
||||||
|
private static Handler mHandler = new Handler();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toast(String msg, int duration) {
|
||||||
|
mHandler.post(() -> Toast.makeText(this, msg, duration).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void toast(int resId, int duration) {
|
||||||
|
mHandler.post(() -> Toast.makeText(this, resId, duration).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
isDarkTheme = prefs.getBoolean("dark_theme", false);
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
devLogging = prefs.getBoolean("developer_logging", false);
|
||||||
|
shellLogging = prefs.getBoolean("shell_logging", false);
|
||||||
|
} else {
|
||||||
|
devLogging = false;
|
||||||
|
shellLogging = false;
|
||||||
|
}
|
||||||
|
magiskHide = prefs.getBoolean("magiskhide", true);
|
||||||
|
updateNotification = prefs.getBoolean("notification", true);
|
||||||
|
initSU();
|
||||||
|
// Always start a new root shell manually, just for safety
|
||||||
|
Shell.init();
|
||||||
|
updateMagiskInfo();
|
||||||
|
// Initialize busybox
|
||||||
|
File busybox = new File(getApplicationInfo().dataDir + "/busybox/busybox");
|
||||||
|
if (!busybox.exists() || !TextUtils.equals(prefs.getString("busybox_version", ""), BUSYBOX_VERSION)) {
|
||||||
|
busybox.getParentFile().mkdirs();
|
||||||
|
Shell.su(
|
||||||
|
"cp -f " + new File(getApplicationInfo().nativeLibraryDir, "libbusybox.so") + " " + busybox,
|
||||||
|
"chmod -R 755 " + busybox.getParent(),
|
||||||
|
busybox + " --install -s " + busybox.getParent()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Initialize prefs
|
||||||
|
prefs.edit()
|
||||||
|
.putBoolean("dark_theme", isDarkTheme)
|
||||||
|
.putBoolean("magiskhide", magiskHide)
|
||||||
|
.putBoolean("notification", updateNotification)
|
||||||
|
.putBoolean("hosts", new File("/magisk/.core/hosts").exists())
|
||||||
|
.putBoolean("disable", Utils.itemExist(MAGISK_DISABLE_FILE))
|
||||||
|
.putBoolean("su_reauth", suReauth)
|
||||||
|
.putString("su_request_timeout", String.valueOf(suRequestTimeout))
|
||||||
|
.putString("su_auto_response", String.valueOf(suResponseType))
|
||||||
|
.putString("su_notification", String.valueOf(suNotificationType))
|
||||||
|
.putString("su_access", String.valueOf(suAccessState))
|
||||||
|
.putString("multiuser_mode", String.valueOf(multiuserMode))
|
||||||
|
.putString("mnt_ns", String.valueOf(suNamespaceMode))
|
||||||
|
.putString("busybox_version", BUSYBOX_VERSION)
|
||||||
|
.apply();
|
||||||
|
// Add busybox to PATH
|
||||||
|
Shell.su("PATH=$PATH:" + busybox.getParent());
|
||||||
|
|
||||||
|
// Create notification channel on Android O
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL,
|
||||||
|
getString(R.string.magisk_updates), NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initSUConfig() {
|
||||||
|
suDB = new SuDatabaseHelper(this);
|
||||||
|
suRequestTimeout = Utils.getPrefsInt(prefs, "su_request_timeout", 10);
|
||||||
|
suResponseType = Utils.getPrefsInt(prefs, "su_auto_response", 0);
|
||||||
|
suNotificationType = Utils.getPrefsInt(prefs, "su_notification", 1);
|
||||||
|
suReauth = prefs.getBoolean("su_reauth", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initSU() {
|
||||||
|
// Create the app data directory, so su binary can work properly
|
||||||
|
new File(getApplicationInfo().dataDir).mkdirs();
|
||||||
|
|
||||||
|
initSUConfig();
|
||||||
|
|
||||||
|
List<String> ret = Shell.sh("su -v");
|
||||||
|
if (Utils.isValidShellResponse(ret)) {
|
||||||
|
suVersion = ret.get(0);
|
||||||
|
isSuClient = suVersion.toUpperCase().contains("MAGISK");
|
||||||
|
}
|
||||||
|
if (isSuClient) {
|
||||||
|
suAccessState = suDB.getSettings(SuDatabaseHelper.ROOT_ACCESS, 3);
|
||||||
|
multiuserMode = suDB.getSettings(SuDatabaseHelper.MULTIUSER_MODE, 0);
|
||||||
|
suNamespaceMode = suDB.getSettings(SuDatabaseHelper.MNT_NS, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateMagiskInfo() {
|
||||||
|
List<String> ret;
|
||||||
|
ret = Shell.sh("magisk -v");
|
||||||
|
if (!Utils.isValidShellResponse(ret)) {
|
||||||
|
ret = Shell.sh("getprop magisk.version");
|
||||||
|
if (Utils.isValidShellResponse(ret)) {
|
||||||
|
try {
|
||||||
|
magiskVersionString = ret.get(0);
|
||||||
|
magiskVersionCode = (int) Double.parseDouble(ret.get(0)) * 10;
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
magiskVersionString = ret.get(0).split(":")[0];
|
||||||
|
ret = Shell.sh("magisk -V");
|
||||||
|
try {
|
||||||
|
magiskVersionCode = Integer.parseInt(ret.get(0));
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
|
}
|
||||||
|
ret = Shell.sh("getprop " + DISABLE_INDICATION_PROP);
|
||||||
|
try {
|
||||||
|
disabled = Utils.isValidShellResponse(ret) && Integer.parseInt(ret.get(0)) != 0;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
disabled = false;
|
||||||
|
}
|
||||||
|
ret = Shell.sh("getprop " + MAGISKHIDE_PROP);
|
||||||
|
try {
|
||||||
|
magiskHide = !Utils.isValidShellResponse(ret) || Integer.parseInt(ret.get(0)) != 0;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
magiskHide = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,65 +1,52 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Fragment;
|
|
||||||
import android.app.FragmentTransaction;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.IdRes;
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.support.v4.view.GravityCompat;
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentTransaction;
|
||||||
import android.support.v4.widget.DrawerLayout;
|
import android.support.v4.widget.DrawerLayout;
|
||||||
import android.support.v7.app.ActionBarDrawerToggle;
|
import android.support.v7.app.ActionBarDrawerToggle;
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.CallbackHandler;
|
import com.topjohnwu.magisk.components.Activity;
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.utils.CallbackEvent;
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity
|
public class MainActivity extends Activity
|
||||||
implements NavigationView.OnNavigationItemSelectedListener, CallbackHandler.EventListener {
|
implements NavigationView.OnNavigationItemSelectedListener, CallbackEvent.Listener<Void> {
|
||||||
|
|
||||||
public static AlertDialog.Builder alertBuilder = null;
|
|
||||||
|
|
||||||
private static final String SELECTED_ITEM_ID = "SELECTED_ITEM_ID";
|
|
||||||
|
|
||||||
private final Handler mDrawerHandler = new Handler();
|
private final Handler mDrawerHandler = new Handler();
|
||||||
private SharedPreferences prefs;
|
private SharedPreferences prefs;
|
||||||
|
private int mDrawerItem;
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
@BindView(R.id.toolbar) Toolbar toolbar;
|
||||||
@BindView(R.id.drawer_layout) DrawerLayout drawer;
|
@BindView(R.id.drawer_layout) DrawerLayout drawer;
|
||||||
@BindView(R.id.nav_view) public NavigationView navigationView;
|
@BindView(R.id.nav_view) public NavigationView navigationView;
|
||||||
|
|
||||||
@IdRes
|
private float toolbarElevation;
|
||||||
private int mSelectedId = R.id.status;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
prefs = getApplicationContext().prefs;
|
||||||
|
|
||||||
String theme = prefs.getString("theme", "");
|
if (getApplicationContext().isDarkTheme) {
|
||||||
Logger.dev("MainActivity: Theme is " + theme);
|
setTheme(R.style.AppTheme_Dark);
|
||||||
if (theme.equals("Dark")) {
|
|
||||||
setTheme(R.style.AppTheme_dh);
|
|
||||||
alertBuilder = new AlertDialog.Builder(this, R.style.AlertDialog_dh);
|
|
||||||
} else {
|
|
||||||
alertBuilder = new AlertDialog.Builder(this);
|
|
||||||
}
|
}
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
@@ -85,49 +72,37 @@ public class MainActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
toolbarElevation = toolbar.getElevation();
|
||||||
|
|
||||||
drawer.addDrawerListener(toggle);
|
drawer.addDrawerListener(toggle);
|
||||||
toggle.syncState();
|
toggle.syncState();
|
||||||
|
|
||||||
//noinspection ResourceType
|
if (savedInstanceState == null)
|
||||||
mSelectedId = savedInstanceState == null ? mSelectedId : savedInstanceState.getInt(SELECTED_ITEM_ID);
|
navigate(getIntent().getStringExtra(MagiskManager.INTENT_SECTION));
|
||||||
navigationView.setCheckedItem(mSelectedId);
|
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
mDrawerHandler.removeCallbacksAndMessages(null);
|
|
||||||
mDrawerHandler.postDelayed(() -> navigate(mSelectedId), 250);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigationView.setNavigationItemSelectedListener(this);
|
navigationView.setNavigationItemSelectedListener(this);
|
||||||
|
getApplicationContext().reloadMainActivity.register(this);
|
||||||
|
|
||||||
if (StatusFragment.updateCheckDone.isTriggered) {
|
|
||||||
onTrigger(StatusFragment.updateCheckDone);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
CallbackHandler.register(StatusFragment.updateCheckDone, this);
|
|
||||||
checkHideSection();
|
checkHideSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
|
getApplicationContext().reloadMainActivity.unRegister(this);
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
CallbackHandler.unRegister(StatusFragment.updateCheckDone, this);
|
|
||||||
alertBuilder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putInt(SELECTED_ITEM_ID, mSelectedId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (drawer.isDrawerOpen(GravityCompat.START)) {
|
if (drawer.isDrawerOpen(navigationView)) {
|
||||||
drawer.closeDrawer(GravityCompat.START);
|
drawer.closeDrawer(navigationView);
|
||||||
|
} else if (mDrawerItem != R.id.magisk) {
|
||||||
|
navigate(R.id.magisk);
|
||||||
} else {
|
} else {
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
@@ -135,70 +110,105 @@ public class MainActivity extends AppCompatActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
|
public boolean onNavigationItemSelected(@NonNull final MenuItem menuItem) {
|
||||||
mSelectedId = menuItem.getItemId();
|
|
||||||
mDrawerHandler.removeCallbacksAndMessages(null);
|
mDrawerHandler.removeCallbacksAndMessages(null);
|
||||||
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
|
mDrawerHandler.postDelayed(() -> navigate(menuItem.getItemId()), 250);
|
||||||
drawer.closeDrawer(GravityCompat.START);
|
drawer.closeDrawer(navigationView);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTrigger(CallbackHandler.Event event) {
|
public void onTrigger(CallbackEvent<Void> event) {
|
||||||
Menu menu = navigationView.getMenu();
|
recreate();
|
||||||
menu.findItem(R.id.install).setVisible(StatusFragment.remoteMagiskVersion > 0 &&
|
|
||||||
Shell.rootAccess());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkHideSection() {
|
public void checkHideSection() {
|
||||||
Menu menu = navigationView.getMenu();
|
Menu menu = navigationView.getMenu();
|
||||||
menu.findItem(R.id.magiskhide).setVisible(StatusFragment.magiskVersion > 0 &&
|
menu.findItem(R.id.magiskhide).setVisible(
|
||||||
prefs.getBoolean("magiskhide", false) && Shell.rootAccess());
|
Shell.rootAccess() && getApplicationContext().magiskVersionCode >= 1300
|
||||||
menu.findItem(R.id.modules).setVisible(StatusFragment.magiskVersion > 0);
|
&& prefs.getBoolean("magiskhide", false));
|
||||||
menu.findItem(R.id.downloads).setVisible(StatusFragment.magiskVersion > 0);
|
menu.findItem(R.id.modules).setVisible(
|
||||||
|
Shell.rootAccess() && getApplicationContext().magiskVersionCode >= 0);
|
||||||
|
menu.findItem(R.id.downloads).setVisible(Utils.checkNetworkStatus(this) &&
|
||||||
|
Shell.rootAccess() && getApplicationContext().magiskVersionCode >= 0);
|
||||||
|
menu.findItem(R.id.log).setVisible(Shell.rootAccess());
|
||||||
|
menu.findItem(R.id.superuser).setVisible(
|
||||||
|
Shell.rootAccess() && getApplicationContext().isSuClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void navigate(final int itemId) {
|
public void navigate(String item) {
|
||||||
Fragment navFragment = null;
|
int itemId = R.id.magisk;
|
||||||
String tag = "";
|
if (item != null) {
|
||||||
|
switch (item) {
|
||||||
|
case "magisk":
|
||||||
|
case "install":
|
||||||
|
itemId = R.id.magisk;
|
||||||
|
break;
|
||||||
|
case "superuser":
|
||||||
|
itemId = R.id.superuser;
|
||||||
|
break;
|
||||||
|
case "modules":
|
||||||
|
itemId = R.id.modules;
|
||||||
|
break;
|
||||||
|
case "downloads":
|
||||||
|
itemId = R.id.downloads;
|
||||||
|
break;
|
||||||
|
case "magiskhide":
|
||||||
|
itemId = R.id.magiskhide;
|
||||||
|
break;
|
||||||
|
case "log":
|
||||||
|
itemId = R.id.log;
|
||||||
|
break;
|
||||||
|
case "settings":
|
||||||
|
itemId = R.id.settings;
|
||||||
|
break;
|
||||||
|
case "about":
|
||||||
|
itemId = R.id.app_about;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
navigate(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void navigate(int itemId) {
|
||||||
|
int bak = mDrawerItem;
|
||||||
|
mDrawerItem = itemId;
|
||||||
|
navigationView.setCheckedItem(itemId);
|
||||||
switch (itemId) {
|
switch (itemId) {
|
||||||
case R.id.status:
|
case R.id.magisk:
|
||||||
tag = "status";
|
displayFragment(new MagiskFragment(), "magisk", true);
|
||||||
navFragment = new StatusFragment();
|
|
||||||
break;
|
break;
|
||||||
case R.id.install:
|
case R.id.superuser:
|
||||||
tag = "install";
|
displayFragment(new SuperuserFragment(), "superuser", true);
|
||||||
navFragment = new InstallFragment();
|
|
||||||
break;
|
break;
|
||||||
case R.id.modules:
|
case R.id.modules:
|
||||||
tag = "modules";
|
displayFragment(new ModulesFragment(), "modules", true);
|
||||||
navFragment = new ModulesFragment();
|
|
||||||
break;
|
break;
|
||||||
case R.id.downloads:
|
case R.id.downloads:
|
||||||
tag = "downloads";
|
displayFragment(new ReposFragment(), "downloads", true);
|
||||||
navFragment = new ReposFragment();
|
|
||||||
break;
|
break;
|
||||||
case R.id.magiskhide:
|
case R.id.magiskhide:
|
||||||
tag = "magiskhide";
|
displayFragment(new MagiskHideFragment(), "magiskhide", true);
|
||||||
navFragment = new MagiskHideFragment();
|
|
||||||
break;
|
break;
|
||||||
case R.id.log:
|
case R.id.log:
|
||||||
tag = "log";
|
displayFragment(new LogFragment(), "log", false);
|
||||||
navFragment = new LogFragment();
|
|
||||||
break;
|
break;
|
||||||
case R.id.settings:
|
case R.id.settings:
|
||||||
startActivity(new Intent(this, SettingsActivity.class));
|
startActivity(new Intent(this, SettingsActivity.class));
|
||||||
|
mDrawerItem = bak;
|
||||||
break;
|
break;
|
||||||
case R.id.app_about:
|
case R.id.app_about:
|
||||||
startActivity(new Intent(this, AboutActivity.class));
|
startActivity(new Intent(this, AboutActivity.class));
|
||||||
return;
|
mDrawerItem = bak;
|
||||||
}
|
break;
|
||||||
|
|
||||||
if (navFragment != null) {
|
|
||||||
FragmentTransaction transaction = getFragmentManager().beginTransaction();
|
|
||||||
transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
|
|
||||||
try {
|
|
||||||
transaction.replace(R.id.content_frame, navFragment, tag).commit();
|
|
||||||
} catch (IllegalStateException ignored) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private void displayFragment(@NonNull Fragment navFragment, String tag, boolean setElevation) {
|
||||||
|
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||||
|
supportInvalidateOptionsMenu();
|
||||||
|
transaction.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||||
|
transaction.replace(R.id.content_frame, navFragment, tag).commitNow();
|
||||||
|
if (setElevation) toolbar.setElevation(toolbarElevation);
|
||||||
|
else toolbar.setElevation(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Fragment;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
import android.support.v4.widget.SwipeRefreshLayout;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -13,39 +13,38 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.github.clans.fab.FloatingActionButton;
|
|
||||||
import com.topjohnwu.magisk.adapters.ModulesAdapter;
|
import com.topjohnwu.magisk.adapters.ModulesAdapter;
|
||||||
|
import com.topjohnwu.magisk.asyncs.FlashZip;
|
||||||
|
import com.topjohnwu.magisk.asyncs.LoadModules;
|
||||||
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
import com.topjohnwu.magisk.module.Module;
|
import com.topjohnwu.magisk.module.Module;
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.utils.CallbackEvent;
|
||||||
import com.topjohnwu.magisk.utils.CallbackHandler;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
import com.topjohnwu.magisk.utils.ModuleHelper;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
public class ModulesFragment extends Fragment implements CallbackHandler.EventListener {
|
public class ModulesFragment extends Fragment implements CallbackEvent.Listener<Void> {
|
||||||
|
|
||||||
public static final CallbackHandler.Event moduleLoadDone = new CallbackHandler.Event();
|
|
||||||
|
|
||||||
private static final int FETCH_ZIP_CODE = 2;
|
private static final int FETCH_ZIP_CODE = 2;
|
||||||
|
|
||||||
|
private Unbinder unbinder;
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
||||||
@BindView(R.id.empty_rv) TextView emptyTv;
|
@BindView(R.id.empty_rv) TextView emptyRv;
|
||||||
@BindView(R.id.fab) FloatingActionButton fabio;
|
@BindView(R.id.fab) FloatingActionButton fabio;
|
||||||
|
|
||||||
private List<Module> listModules = new ArrayList<>();
|
private List<Module> listModules = new ArrayList<>();
|
||||||
private View mView;
|
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
mView = inflater.inflate(R.layout.modules_fragment, container, false);
|
View view = inflater.inflate(R.layout.fragment_modules, container, false);
|
||||||
ButterKnife.bind(this, mView);
|
unbinder = ButterKnife.bind(this, view);
|
||||||
|
|
||||||
fabio.setOnClickListener(v -> {
|
fabio.setOnClickListener(v -> {
|
||||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||||
@@ -55,7 +54,7 @@ public class ModulesFragment extends Fragment implements CallbackHandler.EventLi
|
|||||||
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
||||||
recyclerView.setVisibility(View.GONE);
|
recyclerView.setVisibility(View.GONE);
|
||||||
new Async.LoadModules().exec();
|
new LoadModules(getActivity()).exec();
|
||||||
});
|
});
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
@@ -70,15 +69,15 @@ public class ModulesFragment extends Fragment implements CallbackHandler.EventLi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (moduleLoadDone.isTriggered) {
|
if (getApplication().moduleLoadDone.isTriggered) {
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
return mView;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTrigger(CallbackHandler.Event event) {
|
public void onTrigger(CallbackEvent<Void> event) {
|
||||||
Logger.dev("ModulesFragment: UI refresh triggered");
|
Logger.dev("ModulesFragment: UI refresh triggered");
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -88,32 +87,38 @@ public class ModulesFragment extends Fragment implements CallbackHandler.EventLi
|
|||||||
if (requestCode == FETCH_ZIP_CODE && resultCode == Activity.RESULT_OK && data != null) {
|
if (requestCode == FETCH_ZIP_CODE && resultCode == Activity.RESULT_OK && data != null) {
|
||||||
// Get the URI of the selected file
|
// Get the URI of the selected file
|
||||||
final Uri uri = data.getData();
|
final Uri uri = data.getData();
|
||||||
new Async.FlashZIP(getActivity(), uri).exec();
|
new FlashZip(getActivity(), uri).exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onStart() {
|
||||||
super.onResume();
|
super.onStart();
|
||||||
mView = this.getView();
|
getApplication().moduleLoadDone.register(this);
|
||||||
CallbackHandler.register(moduleLoadDone, this);
|
|
||||||
getActivity().setTitle(R.string.modules);
|
getActivity().setTitle(R.string.modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onStop() {
|
||||||
super.onDestroy();
|
getApplication().moduleLoadDone.unRegister(this);
|
||||||
CallbackHandler.unRegister(moduleLoadDone, this);
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
unbinder.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUI() {
|
private void updateUI() {
|
||||||
ModuleHelper.getModuleList(listModules);
|
listModules.clear();
|
||||||
|
listModules.addAll(getApplication().moduleMap.values());
|
||||||
if (listModules.size() == 0) {
|
if (listModules.size() == 0) {
|
||||||
emptyTv.setVisibility(View.VISIBLE);
|
emptyRv.setVisibility(View.VISIBLE);
|
||||||
recyclerView.setVisibility(View.GONE);
|
recyclerView.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
emptyTv.setVisibility(View.GONE);
|
emptyRv.setVisibility(View.GONE);
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
recyclerView.setVisibility(View.VISIBLE);
|
||||||
recyclerView.setAdapter(new ModulesAdapter(listModules));
|
recyclerView.setAdapter(new ModulesAdapter(listModules));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.view.MenuItemCompat;
|
import android.support.v4.view.MenuItemCompat;
|
||||||
@@ -16,24 +15,26 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import com.topjohnwu.magisk.adapters.ReposAdapter;
|
import com.topjohnwu.magisk.adapters.ReposAdapter;
|
||||||
import com.topjohnwu.magisk.adapters.SimpleSectionedRecyclerViewAdapter;
|
import com.topjohnwu.magisk.adapters.SimpleSectionedRecyclerViewAdapter;
|
||||||
|
import com.topjohnwu.magisk.asyncs.LoadRepos;
|
||||||
|
import com.topjohnwu.magisk.asyncs.ParallelTask;
|
||||||
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
|
import com.topjohnwu.magisk.module.Module;
|
||||||
import com.topjohnwu.magisk.module.Repo;
|
import com.topjohnwu.magisk.module.Repo;
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.utils.CallbackEvent;
|
||||||
import com.topjohnwu.magisk.utils.CallbackHandler;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
import com.topjohnwu.magisk.utils.ModuleHelper;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
public class ReposFragment extends Fragment implements CallbackHandler.EventListener {
|
public class ReposFragment extends Fragment implements CallbackEvent.Listener<Void> {
|
||||||
|
|
||||||
public static final CallbackHandler.Event repoLoadDone = new CallbackHandler.Event();
|
|
||||||
|
|
||||||
|
private Unbinder unbinder;
|
||||||
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
||||||
@BindView(R.id.empty_rv) TextView emptyTv;
|
@BindView(R.id.empty_rv) TextView emptyRv;
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
||||||
|
|
||||||
private List<Repo> mUpdateRepos = new ArrayList<>();
|
private List<Repo> mUpdateRepos = new ArrayList<>();
|
||||||
@@ -47,16 +48,19 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
|
|||||||
|
|
||||||
private SearchView.OnQueryTextListener searchListener;
|
private SearchView.OnQueryTextListener searchListener;
|
||||||
|
|
||||||
@Nullable
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
View view = inflater.inflate(R.layout.repos_fragment, container, false);
|
View view = inflater.inflate(R.layout.fragment_repos, container, false);
|
||||||
|
unbinder = ButterKnife.bind(this, view);
|
||||||
|
|
||||||
ButterKnife.bind(this, view);
|
mSectionedAdapter = new SimpleSectionedRecyclerViewAdapter(R.layout.section,
|
||||||
|
|
||||||
mSectionedAdapter = new
|
|
||||||
SimpleSectionedRecyclerViewAdapter(getActivity(), R.layout.section,
|
|
||||||
R.id.section_text, new ReposAdapter(fUpdateRepos, fInstalledRepos, fOthersRepos));
|
R.id.section_text, new ReposAdapter(fUpdateRepos, fInstalledRepos, fOthersRepos));
|
||||||
|
|
||||||
recyclerView.setAdapter(mSectionedAdapter);
|
recyclerView.setAdapter(mSectionedAdapter);
|
||||||
@@ -65,10 +69,10 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
|
|||||||
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
||||||
recyclerView.setVisibility(View.GONE);
|
recyclerView.setVisibility(View.GONE);
|
||||||
new Async.LoadRepos(getActivity()).exec();
|
new LoadRepos(getActivity()).exec();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (repoLoadDone.isTriggered) {
|
if (getApplication().repoLoadDone.isTriggered) {
|
||||||
reloadRepos();
|
reloadRepos();
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
@@ -90,7 +94,7 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTrigger(CallbackHandler.Event event) {
|
public void onTrigger(CallbackEvent<Void> event) {
|
||||||
Logger.dev("ReposFragment: UI refresh triggered");
|
Logger.dev("ReposFragment: UI refresh triggered");
|
||||||
reloadRepos();
|
reloadRepos();
|
||||||
updateUI();
|
updateUI();
|
||||||
@@ -104,21 +108,40 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onStart() {
|
||||||
super.onResume();
|
super.onStart();
|
||||||
setHasOptionsMenu(true);
|
getApplication().repoLoadDone.register(this);
|
||||||
CallbackHandler.register(repoLoadDone, this);
|
|
||||||
getActivity().setTitle(R.string.downloads);
|
getActivity().setTitle(R.string.downloads);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onStop() {
|
||||||
super.onDestroy();
|
getApplication().repoLoadDone.unRegister(this);
|
||||||
CallbackHandler.unRegister(repoLoadDone, this);
|
super.onStop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
unbinder.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reloadRepos() {
|
private void reloadRepos() {
|
||||||
ModuleHelper.getRepoLists(mUpdateRepos, mInstalledRepos, mOthersRepos);
|
mUpdateRepos.clear();
|
||||||
|
mInstalledRepos.clear();
|
||||||
|
mOthersRepos.clear();
|
||||||
|
for (Repo repo : getApplication().repoMap.values()) {
|
||||||
|
Module module = getApplication().moduleMap.get(repo.getId());
|
||||||
|
if (module != null) {
|
||||||
|
if (repo.getVersionCode() > module.getVersionCode()) {
|
||||||
|
mUpdateRepos.add(repo);
|
||||||
|
} else {
|
||||||
|
mInstalledRepos.add(repo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mOthersRepos.add(repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
fUpdateRepos.clear();
|
fUpdateRepos.clear();
|
||||||
fInstalledRepos.clear();
|
fInstalledRepos.clear();
|
||||||
fOthersRepos.clear();
|
fOthersRepos.clear();
|
||||||
@@ -129,7 +152,7 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
|
|||||||
|
|
||||||
private void updateUI() {
|
private void updateUI() {
|
||||||
if (fUpdateRepos.size() + fInstalledRepos.size() + fOthersRepos.size() == 0) {
|
if (fUpdateRepos.size() + fInstalledRepos.size() + fOthersRepos.size() == 0) {
|
||||||
emptyTv.setVisibility(View.VISIBLE);
|
emptyRv.setVisibility(View.VISIBLE);
|
||||||
recyclerView.setVisibility(View.GONE);
|
recyclerView.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
List<SimpleSectionedRecyclerViewAdapter.Section> sections = new ArrayList<>();
|
List<SimpleSectionedRecyclerViewAdapter.Section> sections = new ArrayList<>();
|
||||||
@@ -144,13 +167,13 @@ public class ReposFragment extends Fragment implements CallbackHandler.EventList
|
|||||||
}
|
}
|
||||||
SimpleSectionedRecyclerViewAdapter.Section[] array = sections.toArray(new SimpleSectionedRecyclerViewAdapter.Section[sections.size()]);
|
SimpleSectionedRecyclerViewAdapter.Section[] array = sections.toArray(new SimpleSectionedRecyclerViewAdapter.Section[sections.size()]);
|
||||||
mSectionedAdapter.setSections(array);
|
mSectionedAdapter.setSections(array);
|
||||||
emptyTv.setVisibility(View.GONE);
|
emptyRv.setVisibility(View.GONE);
|
||||||
recyclerView.setVisibility(View.VISIBLE);
|
recyclerView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
mSwipeRefreshLayout.setRefreshing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class FilterApps extends Async.NormalTask<String, Void, Void> {
|
private class FilterApps extends ParallelTask<String, Void, Void> {
|
||||||
@Override
|
@Override
|
||||||
protected Void doInBackground(String... strings) {
|
protected Void doInBackground(String... strings) {
|
||||||
String newText = strings[0];
|
String newText = strings[0];
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.CheckBoxPreference;
|
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.asyncs.MagiskHide;
|
||||||
|
import com.topjohnwu.magisk.components.Activity;
|
||||||
|
import com.topjohnwu.magisk.components.AlertDialogBuilder;
|
||||||
|
import com.topjohnwu.magisk.database.SuDatabaseHelper;
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
@@ -21,20 +24,18 @@ import com.topjohnwu.magisk.utils.Utils;
|
|||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
public class SettingsActivity extends AppCompatActivity {
|
public class SettingsActivity extends Activity {
|
||||||
|
|
||||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
@BindView(R.id.toolbar) Toolbar toolbar;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
String theme = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("theme", "");
|
if (getApplicationContext().isDarkTheme) {
|
||||||
Logger.dev("AboutActivity: Theme is " + theme);
|
setTheme(R.style.AppTheme_Transparent_Dark);
|
||||||
if (theme.equals("Dark")) {
|
|
||||||
setTheme(R.style.AppTheme_dh);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_container);
|
setContentView(R.layout.activity_settings);
|
||||||
ButterKnife.bind(this);
|
ButterKnife.bind(this);
|
||||||
|
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
@@ -55,49 +56,67 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFloating() {
|
|
||||||
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
|
|
||||||
if (isTablet) {
|
|
||||||
WindowManager.LayoutParams params = getWindow().getAttributes();
|
|
||||||
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
|
|
||||||
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
|
|
||||||
params.alpha = 1.0f;
|
|
||||||
params.dimAmount = 0.6f;
|
|
||||||
params.flags |= 2;
|
|
||||||
getWindow().setAttributes(params);
|
|
||||||
setFinishOnTouchOutside(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment
|
public static class SettingsFragment extends PreferenceFragment
|
||||||
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
implements SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private ListPreference themePreference;
|
|
||||||
private SharedPreferences prefs;
|
private SharedPreferences prefs;
|
||||||
|
private PreferenceScreen prefScreen;
|
||||||
|
|
||||||
|
private ListPreference suAccess, autoRes, suNotification, requestTimeout, multiuserMode, namespaceMode;
|
||||||
|
private MagiskManager magiskManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
addPreferencesFromResource(R.xml.app_settings);
|
addPreferencesFromResource(R.xml.app_settings);
|
||||||
PreferenceManager.setDefaultValues(getActivity(), R.xml.app_settings, false);
|
|
||||||
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||||
|
prefScreen = getPreferenceScreen();
|
||||||
|
magiskManager = Utils.getMagiskManager(getActivity());
|
||||||
|
|
||||||
themePreference = (ListPreference) findPreference("theme");
|
PreferenceCategory magiskCategory = (PreferenceCategory) findPreference("magisk");
|
||||||
CheckBoxPreference busyboxPreference = (CheckBoxPreference) findPreference("busybox");
|
PreferenceCategory suCategory = (PreferenceCategory) findPreference("superuser");
|
||||||
CheckBoxPreference magiskhidePreference = (CheckBoxPreference) findPreference("magiskhide");
|
PreferenceCategory developer = (PreferenceCategory) findPreference("developer");
|
||||||
CheckBoxPreference hostsPreference = (CheckBoxPreference) findPreference("hosts");
|
|
||||||
|
|
||||||
themePreference.setSummary(themePreference.getValue());
|
suAccess = (ListPreference) findPreference("su_access");
|
||||||
|
autoRes = (ListPreference) findPreference("su_auto_response");
|
||||||
|
requestTimeout = (ListPreference) findPreference("su_request_timeout");
|
||||||
|
suNotification = (ListPreference) findPreference("su_notification");
|
||||||
|
multiuserMode = (ListPreference) findPreference("multiuser_mode");
|
||||||
|
namespaceMode = (ListPreference) findPreference("mnt_ns");
|
||||||
|
SwitchPreference reauth = (SwitchPreference) findPreference("su_reauth");
|
||||||
|
|
||||||
if (StatusFragment.magiskVersion < 9) {
|
setSummary();
|
||||||
hostsPreference.setEnabled(false);
|
|
||||||
busyboxPreference.setEnabled(false);
|
// Disable dangerous settings in user mode if selected owner manage
|
||||||
} else if (StatusFragment.magiskVersion < 8) {
|
if (getActivity().getApplicationInfo().uid > 99999) {
|
||||||
magiskhidePreference.setEnabled(false);
|
prefScreen.removePreference(magiskCategory);
|
||||||
|
prefScreen.removePreference(suCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove re-authentication option on Android O, it will not work
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
suCategory.removePreference(reauth);
|
||||||
|
}
|
||||||
|
|
||||||
|
findPreference("clear").setOnPreferenceClickListener((pref) -> {
|
||||||
|
Utils.clearRepoCache(getActivity());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!BuildConfig.DEBUG) {
|
||||||
|
prefScreen.removePreference(developer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Shell.rootAccess()) {
|
||||||
|
prefScreen.removePreference(magiskCategory);
|
||||||
|
prefScreen.removePreference(suCategory);
|
||||||
} else {
|
} else {
|
||||||
busyboxPreference.setEnabled(true);
|
if (!magiskManager.isSuClient) {
|
||||||
magiskhidePreference.setEnabled(true);
|
prefScreen.removePreference(suCategory);
|
||||||
hostsPreference.setEnabled(true);
|
}
|
||||||
|
if (magiskManager.magiskVersionCode < 1300) {
|
||||||
|
prefScreen.removePreference(magiskCategory);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,102 +127,107 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onPause() {
|
||||||
super.onDestroy();
|
super.onPause();
|
||||||
prefs.unregisterOnSharedPreferenceChangeListener(this);
|
prefs.unregisterOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
|
||||||
Logger.dev("Settings: Prefs change " + key);
|
Logger.dev("Settings: Prefs change " + key);
|
||||||
boolean checked;
|
boolean enabled;
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "theme":
|
case "dark_theme":
|
||||||
String theme = prefs.getString(key, "");
|
enabled = prefs.getBoolean("dark_theme", false);
|
||||||
|
if (magiskManager.isDarkTheme != enabled) {
|
||||||
themePreference.setSummary(theme);
|
magiskManager.isDarkTheme = enabled;
|
||||||
if (theme.equals("Dark")) {
|
magiskManager.reloadMainActivity.trigger();
|
||||||
getActivity().getApplication().setTheme(R.style.AppTheme_dh);
|
getActivity().recreate();
|
||||||
} else {
|
|
||||||
getActivity().getApplication().setTheme(R.style.AppTheme);
|
|
||||||
}
|
}
|
||||||
Intent intent = new Intent(getActivity(), MainActivity.class);
|
break;
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
case "disable":
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
enabled = prefs.getBoolean("disable", false);
|
||||||
startActivity(intent);
|
if (enabled) {
|
||||||
|
Utils.createFile(MagiskManager.MAGISK_DISABLE_FILE);
|
||||||
|
} else {
|
||||||
|
Utils.removeItem(MagiskManager.MAGISK_DISABLE_FILE);
|
||||||
|
}
|
||||||
|
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
|
||||||
break;
|
break;
|
||||||
case "magiskhide":
|
case "magiskhide":
|
||||||
checked = prefs.getBoolean("magiskhide", false);
|
enabled = prefs.getBoolean("magiskhide", false);
|
||||||
if (checked) {
|
if (enabled) {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
if (!magiskManager.isSuClient) {
|
||||||
@Override
|
new AlertDialogBuilder(getActivity())
|
||||||
protected Void doInBackground(Void... params) {
|
.setTitle(R.string.no_magisksu_title)
|
||||||
Utils.createFile("/magisk/.core/magiskhide/enable");
|
.setMessage(R.string.no_magisksu_msg)
|
||||||
return null;
|
.setPositiveButton(R.string.understand, (dialog, which) -> new MagiskHide().enable())
|
||||||
}
|
.setCancelable(false)
|
||||||
}.exec();
|
.show();
|
||||||
|
} else {
|
||||||
|
new MagiskHide().enable();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
new MagiskHide().disable();
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
Utils.removeItem("/magisk/.core/magiskhide/enable");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
}
|
||||||
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case "busybox":
|
|
||||||
checked = prefs.getBoolean("busybox", false);
|
|
||||||
if (checked) {
|
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
Utils.createFile("/magisk/.core/busybox/enable");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
} else {
|
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
Utils.removeItem("/magisk/.core/busybox/enable");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
|
||||||
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
break;
|
||||||
case "hosts":
|
case "hosts":
|
||||||
checked = prefs.getBoolean("hosts", false);
|
enabled = prefs.getBoolean("hosts", false);
|
||||||
if (checked) {
|
if (enabled) {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
Shell.su_async(null,
|
||||||
@Override
|
"cp -af /system/etc/hosts /magisk/.core/hosts",
|
||||||
protected Void doInBackground(Void... voids) {
|
"mount -o bind /magisk/.core/hosts /system/etc/hosts");
|
||||||
Shell.su("cp -af /system/etc/hosts /magisk/.core/hosts");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
} else {
|
} else {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
Shell.su_async(null,
|
||||||
@Override
|
"umount -l /system/etc/hosts",
|
||||||
protected Void doInBackground(Void... voids) {
|
"rm -f /magisk/.core/hosts");
|
||||||
Shell.su("umount -l /system/etc/hosts", "rm -f /magisk/.core/hosts");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
}
|
||||||
Toast.makeText(getActivity(), R.string.settings_reboot_toast, Toast.LENGTH_LONG).show();
|
break;
|
||||||
|
case "su_access":
|
||||||
|
magiskManager.suAccessState = Utils.getPrefsInt(prefs, "su_access", 3);
|
||||||
|
magiskManager.suDB.setSettings(SuDatabaseHelper.ROOT_ACCESS, magiskManager.suAccessState);
|
||||||
|
break;
|
||||||
|
case "multiuser_mode":
|
||||||
|
magiskManager.multiuserMode = Utils.getPrefsInt(prefs, "multiuser_mode", 0);
|
||||||
|
magiskManager.suDB.setSettings(SuDatabaseHelper.MULTIUSER_MODE, magiskManager.multiuserMode);
|
||||||
|
break;
|
||||||
|
case "mnt_ns":
|
||||||
|
magiskManager.suNamespaceMode = Utils.getPrefsInt(prefs, "mnt_ns", 1);
|
||||||
|
magiskManager.suDB.setSettings(SuDatabaseHelper.MNT_NS, magiskManager.suNamespaceMode);
|
||||||
|
break;
|
||||||
|
case "su_request_timeout":
|
||||||
|
magiskManager.suRequestTimeout = Utils.getPrefsInt(prefs, "su_request_timeout", 10);
|
||||||
|
break;
|
||||||
|
case "su_auto_response":
|
||||||
|
magiskManager.suResponseType = Utils.getPrefsInt(prefs, "su_auto_response", 0);
|
||||||
|
break;
|
||||||
|
case "su_notification":
|
||||||
|
magiskManager.suNotificationType = Utils.getPrefsInt(prefs, "su_notification", 1);
|
||||||
break;
|
break;
|
||||||
case "developer_logging":
|
case "developer_logging":
|
||||||
Logger.devLog = prefs.getBoolean("developer_logging", false);
|
MagiskManager.devLogging = prefs.getBoolean("developer_logging", false);
|
||||||
break;
|
break;
|
||||||
case "shell_logging":
|
case "shell_logging":
|
||||||
Logger.logShell = prefs.getBoolean("shell_logging", false);
|
MagiskManager.shellLogging = prefs.getBoolean("shell_logging", false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
setSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setSummary() {
|
||||||
|
suAccess.setSummary(getResources()
|
||||||
|
.getStringArray(R.array.su_access)[magiskManager.suAccessState]);
|
||||||
|
autoRes.setSummary(getResources()
|
||||||
|
.getStringArray(R.array.auto_response)[magiskManager.suResponseType]);
|
||||||
|
suNotification.setSummary(getResources()
|
||||||
|
.getStringArray(R.array.su_notification)[magiskManager.suNotificationType]);
|
||||||
|
requestTimeout.setSummary(
|
||||||
|
getString(R.string.request_timeout_summary, prefs.getString("su_request_timeout", "10")));
|
||||||
|
multiuserMode.setSummary(getResources()
|
||||||
|
.getStringArray(R.array.multiuser_summary)[magiskManager.multiuserMode]);
|
||||||
|
namespaceMode.setSummary(getResources()
|
||||||
|
.getStringArray(R.array.namespace_summary)[magiskManager.suNamespaceMode]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,57 @@
|
|||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.app.job.JobInfo;
|
||||||
|
import android.app.job.JobScheduler;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.PreferenceManager;
|
import android.text.TextUtils;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.asyncs.GetBootBlocks;
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.asyncs.LoadApps;
|
||||||
|
import com.topjohnwu.magisk.asyncs.LoadModules;
|
||||||
|
import com.topjohnwu.magisk.asyncs.LoadRepos;
|
||||||
|
import com.topjohnwu.magisk.components.Activity;
|
||||||
|
import com.topjohnwu.magisk.services.UpdateCheckService;
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
public class SplashActivity extends AppCompatActivity {
|
public class SplashActivity extends Activity{
|
||||||
|
|
||||||
|
private static final int UPDATE_SERVICE_ID = 1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplication());
|
|
||||||
if (prefs.getString("theme", "").equals("Dark")) {
|
// Init the info and configs and root shell
|
||||||
setTheme(R.style.AppTheme_dh);
|
getApplicationContext().init();
|
||||||
|
|
||||||
|
// Now fire all async tasks
|
||||||
|
new GetBootBlocks(this).exec();
|
||||||
|
new LoadModules(this).setCallBack(() -> new LoadRepos(this).exec()).exec();
|
||||||
|
new LoadApps(this).exec();
|
||||||
|
|
||||||
|
if (Utils.checkNetworkStatus(this)) {
|
||||||
|
// Initialize the update check service, notify every 8 hours
|
||||||
|
if (!TextUtils.equals("install", getIntent().getStringExtra(MagiskManager.INTENT_SECTION))) {
|
||||||
|
ComponentName service = new ComponentName(this, UpdateCheckService.class);
|
||||||
|
JobInfo jobInfo = new JobInfo.Builder(UPDATE_SERVICE_ID, service)
|
||||||
|
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
||||||
|
.setPersisted(true)
|
||||||
|
.setPeriodic(8 * 60 * 60 * 1000)
|
||||||
|
.build();
|
||||||
|
JobScheduler scheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
|
||||||
|
scheduler.schedule(jobInfo);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.devLog = prefs.getBoolean("developer_logging", false);
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
Logger.logShell = prefs.getBoolean("shell_logging", false);
|
String section = getIntent().getStringExtra(MagiskManager.INTENT_SECTION);
|
||||||
|
if (section != null) {
|
||||||
// Initialize prefs
|
intent.putExtra(MagiskManager.INTENT_SECTION, section);
|
||||||
prefs.edit()
|
}
|
||||||
.putBoolean("magiskhide", Utils.itemExist(false, "/magisk/.core/magiskhide/enable"))
|
|
||||||
.putBoolean("busybox", Utils.commandExists("busybox"))
|
|
||||||
.putBoolean("hosts", Utils.itemExist(false, "/magisk/.core/hosts"))
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
// Start all async tasks
|
|
||||||
new Async.GetBootBlocks().exec();
|
|
||||||
new Async.CheckUpdates().exec();
|
|
||||||
Async.checkSafetyNet(getApplicationContext());
|
|
||||||
new Async.LoadModules() {
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
super.onPostExecute(v);
|
|
||||||
new Async.LoadRepos(getApplicationContext()).exec();
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
|
|
||||||
// Start main activity
|
|
||||||
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,246 +0,0 @@
|
|||||||
package com.topjohnwu.magisk;
|
|
||||||
|
|
||||||
import android.app.Fragment;
|
|
||||||
import android.app.FragmentTransaction;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.widget.SwipeRefreshLayout;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
|
||||||
import com.topjohnwu.magisk.utils.CallbackHandler;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import butterknife.BindColor;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
|
|
||||||
public class StatusFragment extends Fragment implements CallbackHandler.EventListener {
|
|
||||||
|
|
||||||
public static double magiskVersion, remoteMagiskVersion = -1;
|
|
||||||
public static String magiskVersionString, magiskLink, magiskChangelog;
|
|
||||||
public static int SNCheckResult = -1;
|
|
||||||
|
|
||||||
public static final CallbackHandler.Event updateCheckDone = new CallbackHandler.Event();
|
|
||||||
public static final CallbackHandler.Event safetyNetDone = new CallbackHandler.Event();
|
|
||||||
|
|
||||||
@BindView(R.id.swipeRefreshLayout) SwipeRefreshLayout mSwipeRefreshLayout;
|
|
||||||
|
|
||||||
@BindView(R.id.magisk_status_container) View magiskStatusContainer;
|
|
||||||
@BindView(R.id.magisk_status_icon) ImageView magiskStatusIcon;
|
|
||||||
@BindView(R.id.magisk_version) TextView magiskVersionText;
|
|
||||||
@BindView(R.id.magisk_update_status) TextView magiskUpdateText;
|
|
||||||
@BindView(R.id.magisk_check_updates_progress) ProgressBar magiskCheckUpdatesProgress;
|
|
||||||
|
|
||||||
@BindView(R.id.root_status_container) View rootStatusContainer;
|
|
||||||
@BindView(R.id.root_status_icon) ImageView rootStatusIcon;
|
|
||||||
@BindView(R.id.root_status) TextView rootStatusText;
|
|
||||||
@BindView(R.id.root_info) TextView rootInfoText;
|
|
||||||
|
|
||||||
@BindView(R.id.safetyNet_container) View safetyNetContainer;
|
|
||||||
@BindView(R.id.safetyNet_icon) ImageView safetyNetIcon;
|
|
||||||
@BindView(R.id.safetyNet_status) TextView safetyNetStatusText;
|
|
||||||
@BindView(R.id.safetyNet_check_progress) ProgressBar safetyNetProgress;
|
|
||||||
|
|
||||||
@BindColor(R.color.red500) int colorBad;
|
|
||||||
@BindColor(R.color.green500) int colorOK;
|
|
||||||
@BindColor(R.color.yellow500) int colorWarn;
|
|
||||||
@BindColor(R.color.grey500) int colorNeutral;
|
|
||||||
@BindColor(R.color.blue500) int colorInfo;
|
|
||||||
@BindColor(android.R.color.transparent) int trans;
|
|
||||||
|
|
||||||
static {
|
|
||||||
checkMagiskInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
||||||
View v = inflater.inflate(R.layout.status_fragment, container, false);
|
|
||||||
ButterKnife.bind(this, v);
|
|
||||||
|
|
||||||
mSwipeRefreshLayout.setOnRefreshListener(() -> {
|
|
||||||
magiskStatusContainer.setBackgroundColor(trans);
|
|
||||||
magiskStatusIcon.setImageResource(0);
|
|
||||||
magiskUpdateText.setText(R.string.checking_for_updates);
|
|
||||||
magiskCheckUpdatesProgress.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
safetyNetProgress.setVisibility(View.VISIBLE);
|
|
||||||
safetyNetContainer.setBackgroundColor(trans);
|
|
||||||
safetyNetIcon.setImageResource(0);
|
|
||||||
safetyNetStatusText.setText(R.string.checking_safetyNet_status);
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
new Async.CheckUpdates().exec();
|
|
||||||
Async.checkSafetyNet(getActivity());
|
|
||||||
});
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
if (updateCheckDone.isTriggered) {
|
|
||||||
updateCheckUI();
|
|
||||||
}
|
|
||||||
if (safetyNetDone.isTriggered) {
|
|
||||||
updateSafetyNetUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (magiskVersion < 0 && Shell.rootAccess()) {
|
|
||||||
MainActivity.alertBuilder
|
|
||||||
.setTitle(R.string.no_magisk_title)
|
|
||||||
.setMessage(R.string.no_magisk_msg)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(R.string.goto_install, (dialogInterface, i) -> {
|
|
||||||
((MainActivity) getActivity()).navigationView.setCheckedItem(R.id.install);
|
|
||||||
FragmentTransaction transaction = getFragmentManager().beginTransaction();
|
|
||||||
transaction.setCustomAnimations(android.R.animator.fade_in, android.R.animator.fade_out);
|
|
||||||
try {
|
|
||||||
transaction.replace(R.id.content_frame, new InstallFragment(), "install").commit();
|
|
||||||
} catch (IllegalStateException ignored) {}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTrigger(CallbackHandler.Event event) {
|
|
||||||
if (event == updateCheckDone) {
|
|
||||||
Logger.dev("StatusFragment: Update Check UI refresh triggered");
|
|
||||||
updateCheckUI();
|
|
||||||
} else if (event == safetyNetDone) {
|
|
||||||
Logger.dev("StatusFragment: SafetyNet UI refresh triggered");
|
|
||||||
updateSafetyNetUI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
CallbackHandler.register(updateCheckDone, this);
|
|
||||||
CallbackHandler.register(safetyNetDone, this);
|
|
||||||
getActivity().setTitle(R.string.status);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
CallbackHandler.unRegister(updateCheckDone, this);
|
|
||||||
CallbackHandler.unRegister(safetyNetDone, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void checkMagiskInfo() {
|
|
||||||
List<String> ret = Shell.sh("getprop magisk.version");
|
|
||||||
if (ret.get(0).length() == 0) {
|
|
||||||
magiskVersion = -1;
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
magiskVersionString = ret.get(0);
|
|
||||||
magiskVersion = Double.parseDouble(ret.get(0));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
// Custom version don't need to receive updates
|
|
||||||
magiskVersion = Double.POSITIVE_INFINITY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
int image, color;
|
|
||||||
|
|
||||||
checkMagiskInfo();
|
|
||||||
|
|
||||||
if (magiskVersion < 0) {
|
|
||||||
magiskVersionText.setText(R.string.magisk_version_error);
|
|
||||||
} else {
|
|
||||||
magiskVersionText.setText(getString(R.string.magisk_version, magiskVersionString));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Shell.rootStatus == 1) {
|
|
||||||
color = colorOK;
|
|
||||||
image = R.drawable.ic_check_circle;
|
|
||||||
rootStatusText.setText(R.string.proper_root);
|
|
||||||
rootInfoText.setText(Shell.sh("su -v").get(0));
|
|
||||||
|
|
||||||
} else {
|
|
||||||
rootInfoText.setText(R.string.root_info_warning);
|
|
||||||
if (Shell.rootStatus == 0) {
|
|
||||||
color = colorBad;
|
|
||||||
image = R.drawable.ic_cancel;
|
|
||||||
rootStatusText.setText(R.string.not_rooted);
|
|
||||||
} else {
|
|
||||||
color = colorNeutral;
|
|
||||||
image = R.drawable.ic_help;
|
|
||||||
rootStatusText.setText(R.string.root_error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rootStatusContainer.setBackgroundColor(color);
|
|
||||||
rootStatusText.setTextColor(color);
|
|
||||||
rootInfoText.setTextColor(color);
|
|
||||||
rootStatusIcon.setImageResource(image);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCheckUI() {
|
|
||||||
int image, color;
|
|
||||||
|
|
||||||
if (remoteMagiskVersion < 0) {
|
|
||||||
color = colorNeutral;
|
|
||||||
image = R.drawable.ic_help;
|
|
||||||
magiskUpdateText.setText(R.string.cannot_check_updates);
|
|
||||||
} else if (remoteMagiskVersion > magiskVersion) {
|
|
||||||
color = colorInfo;
|
|
||||||
image = R.drawable.ic_update;
|
|
||||||
magiskUpdateText.setText(getString(R.string.magisk_update_available, remoteMagiskVersion));
|
|
||||||
} else {
|
|
||||||
color = colorOK;
|
|
||||||
image = R.drawable.ic_check_circle;
|
|
||||||
magiskUpdateText.setText(getString(R.string.up_to_date, getString(R.string.magisk)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (magiskVersion < 0) {
|
|
||||||
color = colorBad;
|
|
||||||
image = R.drawable.ic_cancel;
|
|
||||||
}
|
|
||||||
magiskStatusContainer.setBackgroundColor(color);
|
|
||||||
magiskVersionText.setTextColor(color);
|
|
||||||
magiskUpdateText.setTextColor(color);
|
|
||||||
magiskStatusIcon.setImageResource(image);
|
|
||||||
|
|
||||||
magiskCheckUpdatesProgress.setVisibility(View.GONE);
|
|
||||||
mSwipeRefreshLayout.setRefreshing(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSafetyNetUI() {
|
|
||||||
int image, color;
|
|
||||||
safetyNetProgress.setVisibility(View.GONE);
|
|
||||||
switch (SNCheckResult) {
|
|
||||||
case -1:
|
|
||||||
color = colorNeutral;
|
|
||||||
image = R.drawable.ic_help;
|
|
||||||
safetyNetStatusText.setText(R.string.safetyNet_error);
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
color = colorBad;
|
|
||||||
image = R.drawable.ic_cancel;
|
|
||||||
safetyNetStatusText.setText(R.string.safetyNet_fail);
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
default:
|
|
||||||
color = colorOK;
|
|
||||||
image = R.drawable.ic_check_circle;
|
|
||||||
safetyNetStatusText.setText(R.string.safetyNet_pass);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
safetyNetContainer.setBackgroundColor(color);
|
|
||||||
safetyNetStatusText.setTextColor(color);
|
|
||||||
safetyNetIcon.setImageResource(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
90
app/src/main/java/com/topjohnwu/magisk/SuLogFragment.java
Normal file
90
app/src/main/java/com/topjohnwu/magisk/SuLogFragment.java
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.adapters.SuLogAdapter;
|
||||||
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
|
import com.topjohnwu.magisk.superuser.SuLogEntry;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
|
public class SuLogFragment extends Fragment {
|
||||||
|
|
||||||
|
@BindView(R.id.empty_rv) TextView emptyRv;
|
||||||
|
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
||||||
|
|
||||||
|
private Unbinder unbinder;
|
||||||
|
private MagiskManager magiskManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
|
inflater.inflate(R.menu.menu_log, menu);
|
||||||
|
menu.findItem(R.id.menu_save).setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
|
Bundle savedInstanceState) {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
View v = inflater.inflate(R.layout.fragment_su_log, container, false);
|
||||||
|
unbinder = ButterKnife.bind(this, v);
|
||||||
|
magiskManager = getApplication();
|
||||||
|
|
||||||
|
updateList();
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateList() {
|
||||||
|
List<SuLogEntry> logs = magiskManager.suDB.getLogList();
|
||||||
|
|
||||||
|
if (logs.size() == 0) {
|
||||||
|
emptyRv.setVisibility(View.VISIBLE);
|
||||||
|
recyclerView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
recyclerView.setAdapter(new SuLogAdapter(logs).getAdapter());
|
||||||
|
emptyRv.setVisibility(View.GONE);
|
||||||
|
recyclerView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case R.id.menu_refresh:
|
||||||
|
updateList();
|
||||||
|
return true;
|
||||||
|
case R.id.menu_clear:
|
||||||
|
magiskManager.suDB.clearLogs();
|
||||||
|
updateList();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
unbinder.unbind();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.topjohnwu.magisk;
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.adapters.PolicyAdapter;
|
||||||
|
import com.topjohnwu.magisk.components.Fragment;
|
||||||
|
import com.topjohnwu.magisk.superuser.Policy;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import butterknife.Unbinder;
|
||||||
|
|
||||||
|
public class SuperuserFragment extends Fragment {
|
||||||
|
|
||||||
|
private Unbinder unbinder;
|
||||||
|
@BindView(R.id.recyclerView) RecyclerView recyclerView;
|
||||||
|
@BindView(R.id.empty_rv) TextView emptyRv;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.fragment_superuser, container, false);
|
||||||
|
unbinder = ButterKnife.bind(this, view);
|
||||||
|
|
||||||
|
PackageManager pm = getActivity().getPackageManager();
|
||||||
|
MagiskManager magiskManager = getApplication();
|
||||||
|
|
||||||
|
List<Policy> policyList = magiskManager.suDB.getPolicyList(pm);
|
||||||
|
|
||||||
|
if (policyList.size() == 0) {
|
||||||
|
emptyRv.setVisibility(View.VISIBLE);
|
||||||
|
recyclerView.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
recyclerView.setAdapter(new PolicyAdapter(policyList, magiskManager.suDB, pm));
|
||||||
|
emptyRv.setVisibility(View.GONE);
|
||||||
|
recyclerView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
getActivity().setTitle(getString(R.string.superuser));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
unbinder.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
package com.topjohnwu.magisk.adapters;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.Filter;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
import com.topjohnwu.magisk.asyncs.MagiskHide;
|
||||||
|
import com.topjohnwu.magisk.components.SnackbarMaker;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
@@ -24,32 +27,38 @@ import butterknife.ButterKnife;
|
|||||||
|
|
||||||
public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.ViewHolder> {
|
public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.ViewHolder> {
|
||||||
|
|
||||||
private List<ApplicationInfo> mList;
|
public static final List<String> BLACKLIST = Arrays.asList(
|
||||||
private List<String> mHideList;
|
"android",
|
||||||
private Context context;
|
|
||||||
private PackageManager packageManager;
|
|
||||||
|
|
||||||
// Don't show in list...
|
|
||||||
private static final String[] blackList = {
|
|
||||||
"com.topjohnwu.magisk",
|
"com.topjohnwu.magisk",
|
||||||
"com.google.android.gms"
|
"com.google.android.gms"
|
||||||
};
|
);
|
||||||
|
|
||||||
public ApplicationAdapter(List<ApplicationInfo> list, List<String> hideList) {
|
private static final List<String> SNLIST = Arrays.asList(
|
||||||
mList = list;
|
"com.google.android.apps.walletnfcrel",
|
||||||
|
"com.nianticlabs.pokemongo"
|
||||||
|
);
|
||||||
|
|
||||||
|
private List<ApplicationInfo> mOriginalList, mList;
|
||||||
|
private List<String> mHideList;
|
||||||
|
private PackageManager packageManager;
|
||||||
|
private ApplicationFilter filter;
|
||||||
|
|
||||||
|
public ApplicationAdapter(PackageManager packageManager) {
|
||||||
|
mOriginalList = mList = Collections.emptyList();
|
||||||
|
mHideList = Collections.emptyList();
|
||||||
|
this.packageManager = packageManager;
|
||||||
|
filter = new ApplicationFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLists(List<ApplicationInfo> listApps, List<String> hideList) {
|
||||||
|
mOriginalList = mList = listApps;
|
||||||
mHideList = hideList;
|
mHideList = hideList;
|
||||||
List<String> bl = Arrays.asList(blackList);
|
notifyDataSetChanged();
|
||||||
for (int i = 0; i < mList.size(); ++i)
|
|
||||||
if (bl.contains(mList.get(i).packageName))
|
|
||||||
mList.remove(i);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
View mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false);
|
View mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_app, parent, false);
|
||||||
context = parent.getContext();
|
|
||||||
packageManager = context.getPackageManager();
|
|
||||||
ButterKnife.bind(this, mView);
|
|
||||||
return new ViewHolder(mView);
|
return new ViewHolder(mView);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,26 +69,31 @@ public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.
|
|||||||
holder.appIcon.setImageDrawable(info.loadIcon(packageManager));
|
holder.appIcon.setImageDrawable(info.loadIcon(packageManager));
|
||||||
holder.appName.setText(info.loadLabel(packageManager));
|
holder.appName.setText(info.loadLabel(packageManager));
|
||||||
holder.appPackage.setText(info.packageName);
|
holder.appPackage.setText(info.packageName);
|
||||||
holder.checkBox.setChecked(false);
|
|
||||||
|
|
||||||
for (String hidePackage : mHideList) {
|
// Remove all listeners
|
||||||
if (info.packageName.contains(hidePackage)) {
|
holder.itemView.setOnClickListener(null);
|
||||||
holder.checkBox.setChecked(true);
|
holder.checkBox.setOnCheckedChangeListener(null);
|
||||||
break;
|
|
||||||
}
|
if (SNLIST.contains(info.packageName)) {
|
||||||
|
holder.checkBox.setChecked(true);
|
||||||
|
holder.checkBox.setEnabled(false);
|
||||||
|
holder.itemView.setOnClickListener(v ->
|
||||||
|
SnackbarMaker.make(holder.itemView,
|
||||||
|
R.string.safetyNet_hide_notice, Snackbar.LENGTH_LONG).show()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
holder.checkBox.setEnabled(true);
|
||||||
|
holder.checkBox.setChecked(mHideList.contains(info.packageName));
|
||||||
|
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
||||||
|
if (isChecked) {
|
||||||
|
new MagiskHide().add(info.packageName);
|
||||||
|
mHideList.add(info.packageName);
|
||||||
|
} else {
|
||||||
|
new MagiskHide().rm(info.packageName);
|
||||||
|
mHideList.remove(info.packageName);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.checkBox.setOnClickListener(v -> {
|
|
||||||
CheckBox chkbox = (CheckBox) v;
|
|
||||||
if (chkbox.isChecked()) {
|
|
||||||
new Async.MagiskHide().add(info.packageName);
|
|
||||||
mHideList.add(info.packageName);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
new Async.MagiskHide().rm(info.packageName);
|
|
||||||
mHideList.remove(info.packageName);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -87,19 +101,52 @@ public class ApplicationAdapter extends RecyclerView.Adapter<ApplicationAdapter.
|
|||||||
return mList.size();
|
return mList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder {
|
public void filter(String constraint) {
|
||||||
|
filter.filter(constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
@BindView(R.id.app_icon) ImageView appIcon;
|
@BindView(R.id.app_icon) ImageView appIcon;
|
||||||
@BindView(R.id.app_name) TextView appName;
|
@BindView(R.id.app_name) TextView appName;
|
||||||
@BindView(R.id.app_package) TextView appPackage;
|
@BindView(R.id.app_package) TextView appPackage;
|
||||||
@BindView(R.id.checkbox) CheckBox checkBox;
|
@BindView(R.id.checkbox) CheckBox checkBox;
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
ViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
WindowManager windowmanager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
|
||||||
ButterKnife.bind(this, itemView);
|
ButterKnife.bind(this, itemView);
|
||||||
DisplayMetrics dimension = new DisplayMetrics();
|
|
||||||
windowmanager.getDefaultDisplay().getMetrics(dimension);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private class ApplicationFilter extends Filter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected FilterResults performFiltering(CharSequence constraint) {
|
||||||
|
List<ApplicationInfo> filteredApps;
|
||||||
|
if (constraint == null || constraint.length() == 0) {
|
||||||
|
filteredApps = mOriginalList;
|
||||||
|
} else {
|
||||||
|
filteredApps = new ArrayList<>();
|
||||||
|
String filter = constraint.toString().toLowerCase();
|
||||||
|
for (ApplicationInfo info : mOriginalList) {
|
||||||
|
if (Utils.lowercaseContains(info.loadLabel(packageManager), filter)
|
||||||
|
|| Utils.lowercaseContains(info.packageName, filter)) {
|
||||||
|
filteredApps.add(info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilterResults results = new FilterResults();
|
||||||
|
results.values = filteredApps;
|
||||||
|
results.count = filteredApps.size();
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
protected void publishResults(CharSequence constraint, FilterResults results) {
|
||||||
|
mList = (List<ApplicationInfo>) results.values;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ package com.topjohnwu.magisk.adapters;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.DisplayMetrics;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.CheckBox;
|
import android.widget.CheckBox;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.components.SnackbarMaker;
|
||||||
import com.topjohnwu.magisk.module.Module;
|
import com.topjohnwu.magisk.module.Module;
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -25,7 +24,6 @@ import butterknife.ButterKnife;
|
|||||||
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHolder> {
|
||||||
|
|
||||||
private final List<Module> mList;
|
private final List<Module> mList;
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public ModulesAdapter(List<Module> list) {
|
public ModulesAdapter(List<Module> list) {
|
||||||
mList = list;
|
mList = list;
|
||||||
@@ -34,90 +32,50 @@ public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHold
|
|||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_module, parent, false);
|
||||||
context = parent.getContext();
|
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||||
|
Context context = holder.itemView.getContext();
|
||||||
final Module module = mList.get(position);
|
final Module module = mList.get(position);
|
||||||
holder.title.setText(module.getName());
|
|
||||||
|
String version = module.getVersion();
|
||||||
String author = module.getAuthor();
|
String author = module.getAuthor();
|
||||||
String versionName = module.getVersion();
|
|
||||||
String description = module.getDescription();
|
String description = module.getDescription();
|
||||||
if (versionName != null) {
|
String noInfo = context.getString(R.string.no_info_provided);
|
||||||
holder.versionName.setText(versionName);
|
|
||||||
}
|
|
||||||
if (author != null) {
|
|
||||||
holder.author.setText(context.getString(R.string.author, author));
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
holder.description.setText(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
holder.title.setText(module.getName());
|
||||||
|
holder.versionName.setText( TextUtils.isEmpty(version) ? noInfo : version);
|
||||||
|
holder.author.setText( TextUtils.isEmpty(author) ? noInfo : context.getString(R.string.author, author));
|
||||||
|
holder.description.setText( TextUtils.isEmpty(description) ? noInfo : description);
|
||||||
|
|
||||||
|
holder.checkBox.setOnCheckedChangeListener(null);
|
||||||
holder.checkBox.setChecked(module.isEnabled());
|
holder.checkBox.setChecked(module.isEnabled());
|
||||||
holder.checkBox.setOnClickListener((v) -> {
|
holder.checkBox.setOnCheckedChangeListener((v, isChecked) -> {
|
||||||
CheckBox checkBox = (CheckBox) v;
|
int snack;
|
||||||
if (checkBox.isChecked()) {
|
if (isChecked) {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
module.removeDisableFile();
|
||||||
@Override
|
snack = R.string.disable_file_removed;
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
module.removeDisableFile();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
Snackbar.make(holder.title, R.string.disable_file_removed, Snackbar.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
} else {
|
} else {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
module.createDisableFile();
|
||||||
@Override
|
snack = R.string.disable_file_created;
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
module.createDisableFile();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
Snackbar.make(holder.title, R.string.disable_file_created, Snackbar.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
}
|
||||||
|
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
||||||
});
|
});
|
||||||
|
|
||||||
holder.delete.setOnClickListener(v -> {
|
holder.delete.setOnClickListener(v -> {
|
||||||
if (module.willBeRemoved()) {
|
boolean removed = module.willBeRemoved();
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
int snack;
|
||||||
@Override
|
if (removed) {
|
||||||
protected Void doInBackground(Void... voids) {
|
module.deleteRemoveFile();
|
||||||
module.deleteRemoveFile();
|
snack = R.string.remove_file_deleted;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
Snackbar.make(holder.title, R.string.remove_file_deleted, Snackbar.LENGTH_SHORT).show();
|
|
||||||
updateDeleteButton(holder, module);
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
} else {
|
} else {
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
module.createRemoveFile();
|
||||||
@Override
|
snack = R.string.remove_file_created;
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
module.createRemoveFile();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
Snackbar.make(holder.title, R.string.remove_file_created, Snackbar.LENGTH_SHORT).show();
|
|
||||||
updateDeleteButton(holder, module);
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
}
|
||||||
|
SnackbarMaker.make(holder.itemView, snack, Snackbar.LENGTH_SHORT).show();
|
||||||
|
updateDeleteButton(holder, module);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (module.isUpdated()) {
|
if (module.isUpdated()) {
|
||||||
@@ -144,7 +102,7 @@ public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHold
|
|||||||
return mList.size();
|
return mList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder {
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
@BindView(R.id.title) TextView title;
|
@BindView(R.id.title) TextView title;
|
||||||
@BindView(R.id.version_name) TextView versionName;
|
@BindView(R.id.version_name) TextView versionName;
|
||||||
@@ -154,12 +112,9 @@ public class ModulesAdapter extends RecyclerView.Adapter<ModulesAdapter.ViewHold
|
|||||||
@BindView(R.id.author) TextView author;
|
@BindView(R.id.author) TextView author;
|
||||||
@BindView(R.id.delete) ImageView delete;
|
@BindView(R.id.delete) ImageView delete;
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
ViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
WindowManager windowmanager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
|
||||||
ButterKnife.bind(this, itemView);
|
ButterKnife.bind(this, itemView);
|
||||||
DisplayMetrics dimension = new DisplayMetrics();
|
|
||||||
windowmanager.getDefaultDisplay().getMetrics(dimension);
|
|
||||||
|
|
||||||
if (!Shell.rootAccess()) {
|
if (!Shell.rootAccess()) {
|
||||||
checkBox.setEnabled(false);
|
checkBox.setEnabled(false);
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package com.topjohnwu.magisk.adapters;
|
||||||
|
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.Switch;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.components.AlertDialogBuilder;
|
||||||
|
import com.topjohnwu.magisk.components.ExpandableViewHolder;
|
||||||
|
import com.topjohnwu.magisk.components.SnackbarMaker;
|
||||||
|
import com.topjohnwu.magisk.database.SuDatabaseHelper;
|
||||||
|
import com.topjohnwu.magisk.superuser.Policy;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
|
public class PolicyAdapter extends RecyclerView.Adapter<PolicyAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private List<Policy> policyList;
|
||||||
|
private SuDatabaseHelper dbHelper;
|
||||||
|
private PackageManager pm;
|
||||||
|
private Set<Policy> expandList = new HashSet<>();
|
||||||
|
|
||||||
|
public PolicyAdapter(List<Policy> list, SuDatabaseHelper db, PackageManager pm) {
|
||||||
|
policyList = list;
|
||||||
|
dbHelper = db;
|
||||||
|
this.pm = pm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_policy, parent, false);
|
||||||
|
return new ViewHolder(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ViewHolder holder, int position) {
|
||||||
|
Policy policy = policyList.get(position);
|
||||||
|
|
||||||
|
holder.setExpanded(expandList.contains(policy));
|
||||||
|
|
||||||
|
holder.itemView.setOnClickListener(view -> {
|
||||||
|
if (holder.mExpanded) {
|
||||||
|
holder.collapse();
|
||||||
|
expandList.remove(policy);
|
||||||
|
} else {
|
||||||
|
holder.expand();
|
||||||
|
expandList.add(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
holder.appName.setText(policy.appName);
|
||||||
|
holder.packageName.setText(policy.packageName);
|
||||||
|
holder.appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
||||||
|
holder.masterSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||||
|
if ((isChecked && policy.policy == Policy.DENY) ||
|
||||||
|
(!isChecked && policy.policy == Policy.ALLOW)) {
|
||||||
|
policy.policy = isChecked ? Policy.ALLOW : Policy.DENY;
|
||||||
|
String message = v.getContext().getString(
|
||||||
|
isChecked ? R.string.su_snack_grant : R.string.su_snack_deny, policy.appName);
|
||||||
|
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||||
|
dbHelper.updatePolicy(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.notificationSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||||
|
if ((isChecked && !policy.notification) ||
|
||||||
|
(!isChecked && policy.notification)) {
|
||||||
|
policy.notification = isChecked;
|
||||||
|
String message = v.getContext().getString(
|
||||||
|
isChecked ? R.string.su_snack_notif_on : R.string.su_snack_notif_off, policy.appName);
|
||||||
|
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||||
|
dbHelper.updatePolicy(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.loggingSwitch.setOnCheckedChangeListener((v, isChecked) -> {
|
||||||
|
if ((isChecked && !policy.logging) ||
|
||||||
|
(!isChecked && policy.logging)) {
|
||||||
|
policy.logging = isChecked;
|
||||||
|
String message = v.getContext().getString(
|
||||||
|
isChecked ? R.string.su_snack_log_on : R.string.su_snack_log_off, policy.appName);
|
||||||
|
SnackbarMaker.make(holder.itemView, message, Snackbar.LENGTH_SHORT).show();
|
||||||
|
dbHelper.updatePolicy(policy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.delete.setOnClickListener(v -> new AlertDialogBuilder(v.getContext())
|
||||||
|
.setTitle(R.string.su_revoke_title)
|
||||||
|
.setMessage(v.getContext().getString(R.string.su_revoke_msg, policy.appName))
|
||||||
|
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||||
|
policyList.remove(position);
|
||||||
|
notifyItemRemoved(position);
|
||||||
|
notifyItemRangeChanged(position, policyList.size());
|
||||||
|
SnackbarMaker.make(holder.itemView, v.getContext().getString(R.string.su_snack_revoke, policy.appName),
|
||||||
|
Snackbar.LENGTH_SHORT).show();
|
||||||
|
dbHelper.deletePolicy(policy);
|
||||||
|
})
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.setCancelable(true)
|
||||||
|
.show());
|
||||||
|
holder.masterSwitch.setChecked(policy.policy == Policy.ALLOW);
|
||||||
|
holder.notificationSwitch.setChecked(policy.notification);
|
||||||
|
holder.loggingSwitch.setChecked(policy.logging);
|
||||||
|
|
||||||
|
// Hide for now
|
||||||
|
holder.moreInfo.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return policyList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends ExpandableViewHolder {
|
||||||
|
|
||||||
|
@BindView(R.id.app_name) TextView appName;
|
||||||
|
@BindView(R.id.package_name) TextView packageName;
|
||||||
|
@BindView(R.id.app_icon) ImageView appIcon;
|
||||||
|
@BindView(R.id.master_switch) Switch masterSwitch;
|
||||||
|
@BindView(R.id.notification_switch) Switch notificationSwitch;
|
||||||
|
@BindView(R.id.logging_switch) Switch loggingSwitch;
|
||||||
|
|
||||||
|
@BindView(R.id.delete) ImageView delete;
|
||||||
|
@BindView(R.id.more_info) ImageView moreInfo;
|
||||||
|
|
||||||
|
public ViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
ButterKnife.bind(this, itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setExpandLayout(View itemView) {
|
||||||
|
expandLayout = itemView.findViewById(R.id.expand_layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,24 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
package com.topjohnwu.magisk.adapters;
|
||||||
|
|
||||||
import android.animation.Animator;
|
import android.app.Activity;
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.animation.ValueAnimator;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.DisplayMetrics;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewTreeObserver;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.MainActivity;
|
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.asyncs.ProcessRepoZip;
|
||||||
|
import com.topjohnwu.magisk.components.AlertDialogBuilder;
|
||||||
|
import com.topjohnwu.magisk.components.MarkDownWindow;
|
||||||
import com.topjohnwu.magisk.module.Repo;
|
import com.topjohnwu.magisk.module.Repo;
|
||||||
import com.topjohnwu.magisk.receivers.RepoDlReceiver;
|
import com.topjohnwu.magisk.receivers.DownloadReceiver;
|
||||||
import com.topjohnwu.magisk.utils.Utils;
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
import com.topjohnwu.magisk.utils.WebWindow;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -32,7 +28,6 @@ import butterknife.ButterKnife;
|
|||||||
public class ReposAdapter extends RecyclerView.Adapter<ReposAdapter.ViewHolder> {
|
public class ReposAdapter extends RecyclerView.Adapter<ReposAdapter.ViewHolder> {
|
||||||
|
|
||||||
private List<Repo> mUpdateRepos, mInstalledRepos, mOthersRepos;
|
private List<Repo> mUpdateRepos, mInstalledRepos, mOthersRepos;
|
||||||
private View mView;
|
|
||||||
private Context mContext;
|
private Context mContext;
|
||||||
|
|
||||||
public ReposAdapter(List<Repo> update, List<Repo> installed, List<Repo> others) {
|
public ReposAdapter(List<Repo> update, List<Repo> installed, List<Repo> others) {
|
||||||
@@ -43,71 +38,54 @@ public class ReposAdapter extends RecyclerView.Adapter<ReposAdapter.ViewHolder>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
mView = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_repo, parent, false);
|
|
||||||
ButterKnife.bind(this, mView);
|
|
||||||
mContext = parent.getContext();
|
mContext = parent.getContext();
|
||||||
|
View v = LayoutInflater.from(mContext).inflate(R.layout.list_item_repo, parent, false);
|
||||||
return new ViewHolder(mView);
|
return new ViewHolder(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(final ViewHolder holder, int position) {
|
public void onBindViewHolder(final ViewHolder holder, int position) {
|
||||||
final Repo repo;
|
Repo repo = getItem(position);
|
||||||
if (position >= mUpdateRepos.size()) {
|
|
||||||
position -= mUpdateRepos.size();
|
|
||||||
if (position >= mInstalledRepos.size()) {
|
|
||||||
position -= mInstalledRepos.size();
|
|
||||||
repo = mOthersRepos.get(position);
|
|
||||||
} else {
|
|
||||||
repo = mInstalledRepos.get(position);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
repo = mUpdateRepos.get(position);
|
|
||||||
}
|
|
||||||
holder.title.setText(repo.getName());
|
holder.title.setText(repo.getName());
|
||||||
|
holder.versionName.setText(repo.getVersion());
|
||||||
String author = repo.getAuthor();
|
String author = repo.getAuthor();
|
||||||
String versionName = repo.getVersion();
|
holder.author.setText(TextUtils.isEmpty(author) ? null : mContext.getString(R.string.author, author));
|
||||||
String description = repo.getDescription();
|
holder.description.setText(repo.getDescription());
|
||||||
if (versionName != null) {
|
|
||||||
holder.versionName.setText(versionName);
|
|
||||||
}
|
|
||||||
if (author != null) {
|
|
||||||
holder.author.setText(mContext.getString(R.string.author, author));
|
|
||||||
}
|
|
||||||
if (description != null) {
|
|
||||||
holder.description.setText(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
View.OnClickListener listener = view -> {
|
holder.infoLayout.setOnClickListener(v -> new MarkDownWindow(null, repo.getDetailUrl(), mContext));
|
||||||
if (view.getId() == holder.updateImage.getId()) {
|
|
||||||
String filename = repo.getName() + "-" + repo.getVersion() + ".zip";
|
|
||||||
MainActivity.alertBuilder
|
|
||||||
.setTitle(mContext.getString(R.string.repo_install_title, repo.getName()))
|
|
||||||
.setMessage(mContext.getString(R.string.repo_install_msg, filename))
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(R.string.download_install, (dialogInterface, i) -> Utils.dlAndReceive(
|
|
||||||
mContext,
|
|
||||||
new RepoDlReceiver(),
|
|
||||||
repo.getZipUrl(),
|
|
||||||
Utils.getLegalFilename(filename)))
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
if ((view.getId() == holder.changeLog.getId()) && (!repo.getLogUrl().equals(""))) {
|
|
||||||
new WebWindow(mContext.getString(R.string.changelog), repo.getLogUrl(), mContext);
|
|
||||||
}
|
|
||||||
if ((view.getId() == holder.authorLink.getId()) && (!repo.getSupportUrl().equals(""))) {
|
|
||||||
mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(repo.getDonateUrl())));
|
|
||||||
}
|
|
||||||
if ((view.getId() == holder.supportLink.getId()) && (!repo.getSupportUrl().equals(""))) {
|
|
||||||
mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(repo.getSupportUrl())));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
holder.changeLog.setOnClickListener(listener);
|
holder.downloadImage.setOnClickListener(v -> {
|
||||||
holder.updateImage.setOnClickListener(listener);
|
String filename = repo.getName() + "-" + repo.getVersion() + ".zip";
|
||||||
holder.authorLink.setOnClickListener(listener);
|
new AlertDialogBuilder(mContext)
|
||||||
holder.supportLink.setOnClickListener(listener);
|
.setTitle(mContext.getString(R.string.repo_install_title, repo.getName()))
|
||||||
|
.setMessage(mContext.getString(R.string.repo_install_msg, filename))
|
||||||
|
.setCancelable(true)
|
||||||
|
.setPositiveButton(R.string.install, (d, i) -> Utils.dlAndReceive(
|
||||||
|
mContext,
|
||||||
|
new DownloadReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onDownloadDone(Uri uri) {
|
||||||
|
Activity activity = (Activity) mContext;
|
||||||
|
new ProcessRepoZip(activity, uri, true).exec();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
repo.getZipUrl(),
|
||||||
|
Utils.getLegalFilename(filename)))
|
||||||
|
.setNeutralButton(R.string.download, (d, i) -> Utils.dlAndReceive(
|
||||||
|
mContext,
|
||||||
|
new DownloadReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onDownloadDone(Uri uri) {
|
||||||
|
Activity activity = (Activity) mContext;
|
||||||
|
new ProcessRepoZip(activity, uri, false).exec();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
repo.getZipUrl(),
|
||||||
|
Utils.getLegalFilename(filename)))
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.show();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -115,104 +93,32 @@ public class ReposAdapter extends RecyclerView.Adapter<ReposAdapter.ViewHolder>
|
|||||||
return mUpdateRepos.size() + mInstalledRepos.size() + mOthersRepos.size();
|
return mUpdateRepos.size() + mInstalledRepos.size() + mOthersRepos.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewHolder extends RecyclerView.ViewHolder {
|
private Repo getItem(int position) {
|
||||||
|
if (position >= mUpdateRepos.size()) {
|
||||||
|
position -= mUpdateRepos.size();
|
||||||
|
if (position >= mInstalledRepos.size()) {
|
||||||
|
position -= mInstalledRepos.size();
|
||||||
|
return mOthersRepos.get(position);
|
||||||
|
} else {
|
||||||
|
return mInstalledRepos.get(position);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return mUpdateRepos.get(position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
@BindView(R.id.title) TextView title;
|
@BindView(R.id.title) TextView title;
|
||||||
@BindView(R.id.version_name) TextView versionName;
|
@BindView(R.id.version_name) TextView versionName;
|
||||||
@BindView(R.id.description) TextView description;
|
@BindView(R.id.description) TextView description;
|
||||||
@BindView(R.id.author) TextView author;
|
@BindView(R.id.author) TextView author;
|
||||||
@BindView(R.id.expand_layout) LinearLayout expandLayout;
|
@BindView(R.id.info_layout) LinearLayout infoLayout;
|
||||||
@BindView(R.id.update) ImageView updateImage;
|
@BindView(R.id.download) ImageView downloadImage;
|
||||||
@BindView(R.id.installed) ImageView installedImage;
|
|
||||||
@BindView(R.id.changeLog) ImageView changeLog;
|
|
||||||
@BindView(R.id.authorLink) ImageView authorLink;
|
|
||||||
@BindView(R.id.supportLink) ImageView supportLink;
|
|
||||||
|
|
||||||
private ValueAnimator mAnimator;
|
ViewHolder(View itemView) {
|
||||||
private ObjectAnimator animY2;
|
|
||||||
private ViewHolder holder;
|
|
||||||
|
|
||||||
private boolean expanded = false;
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
super(itemView);
|
||||||
WindowManager windowmanager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
|
|
||||||
ButterKnife.bind(this, itemView);
|
ButterKnife.bind(this, itemView);
|
||||||
DisplayMetrics dimension = new DisplayMetrics();
|
|
||||||
windowmanager.getDefaultDisplay().getMetrics(dimension);
|
|
||||||
holder = this;
|
|
||||||
this.expandLayout.getViewTreeObserver().addOnPreDrawListener(
|
|
||||||
new ViewTreeObserver.OnPreDrawListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPreDraw() {
|
|
||||||
final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
holder.expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
|
|
||||||
holder.expandLayout.setVisibility(View.GONE);
|
|
||||||
holder.expandLayout.measure(widthSpec, heightSpec);
|
|
||||||
final int holderHeight = holder.expandLayout.getMeasuredHeight();
|
|
||||||
mAnimator = slideAnimator(0, holderHeight);
|
|
||||||
animY2 = ObjectAnimator.ofFloat(holder.updateImage, "translationY", holderHeight / 2);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
mView.setOnClickListener(view -> {
|
|
||||||
if (expanded) {
|
|
||||||
collapse(holder.expandLayout);
|
|
||||||
} else {
|
|
||||||
expand(holder.expandLayout);
|
|
||||||
}
|
|
||||||
expanded = !expanded;
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void expand(View view) {
|
|
||||||
view.setVisibility(View.VISIBLE);
|
|
||||||
mAnimator.start();
|
|
||||||
animY2.start();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void collapse(View view) {
|
|
||||||
int finalHeight = view.getHeight();
|
|
||||||
ValueAnimator mAnimator = slideAnimator(finalHeight, 0);
|
|
||||||
mAnimator.addListener(new Animator.AnimatorListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animator) {
|
|
||||||
view.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animator animator) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animator) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animator animator) {}
|
|
||||||
});
|
|
||||||
mAnimator.start();
|
|
||||||
animY2.reverse();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private ValueAnimator slideAnimator(int start, int end) {
|
|
||||||
|
|
||||||
ValueAnimator animator = ValueAnimator.ofInt(start, end);
|
|
||||||
|
|
||||||
animator.addUpdateListener(valueAnimator -> {
|
|
||||||
int value = (Integer) valueAnimator.getAnimatedValue();
|
|
||||||
ViewGroup.LayoutParams layoutParams = expandLayout
|
|
||||||
.getLayoutParams();
|
|
||||||
layoutParams.height = value;
|
|
||||||
expandLayout.setLayoutParams(layoutParams);
|
|
||||||
});
|
|
||||||
return animator;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.topjohnwu.magisk.adapters;
|
package com.topjohnwu.magisk.adapters;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.util.SparseArray;
|
import android.util.SparseArray;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -13,25 +12,21 @@ import java.util.Comparator;
|
|||||||
|
|
||||||
public class SimpleSectionedRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
public class SimpleSectionedRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
private static final int SECTION_TYPE = 0;
|
private static final int SECTION_TYPE = 0;
|
||||||
|
|
||||||
private boolean mValid = true;
|
private boolean mValid = true;
|
||||||
private int mSectionResourceId;
|
private int mSectionResourceId;
|
||||||
private int mTextResourceId;
|
private int mTextResourceId;
|
||||||
private LayoutInflater mLayoutInflater;
|
|
||||||
private RecyclerView.Adapter mBaseAdapter;
|
private RecyclerView.Adapter mBaseAdapter;
|
||||||
private SparseArray<Section> mSections = new SparseArray<Section>();
|
private SparseArray<Section> mSections = new SparseArray<Section>();
|
||||||
|
|
||||||
|
|
||||||
public SimpleSectionedRecyclerViewAdapter(Context context, int sectionResourceId, int textResourceId,
|
public SimpleSectionedRecyclerViewAdapter(int sectionResourceId, int textResourceId,
|
||||||
RecyclerView.Adapter baseAdapter) {
|
RecyclerView.Adapter baseAdapter) {
|
||||||
|
|
||||||
mLayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
mSectionResourceId = sectionResourceId;
|
mSectionResourceId = sectionResourceId;
|
||||||
mTextResourceId = textResourceId;
|
mTextResourceId = textResourceId;
|
||||||
mBaseAdapter = baseAdapter;
|
mBaseAdapter = baseAdapter;
|
||||||
mContext = context;
|
|
||||||
|
|
||||||
mBaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
mBaseAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
|
||||||
@Override
|
@Override
|
||||||
@@ -74,7 +69,7 @@ public class SimpleSectionedRecyclerViewAdapter extends RecyclerView.Adapter<Rec
|
|||||||
@Override
|
@Override
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int typeView) {
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int typeView) {
|
||||||
if (typeView == SECTION_TYPE) {
|
if (typeView == SECTION_TYPE) {
|
||||||
final View view = LayoutInflater.from(mContext).inflate(mSectionResourceId, parent, false);
|
View view = LayoutInflater.from(parent.getContext()).inflate(mSectionResourceId, parent, false);
|
||||||
return new SectionViewHolder(view,mTextResourceId);
|
return new SectionViewHolder(view,mTextResourceId);
|
||||||
}else{
|
}else{
|
||||||
return mBaseAdapter.onCreateViewHolder(parent, typeView -1);
|
return mBaseAdapter.onCreateViewHolder(parent, typeView -1);
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
package com.topjohnwu.magisk.adapters;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.RotateAnimation;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.thoughtbot.expandablerecyclerview.ExpandableRecyclerViewAdapter;
|
||||||
|
import com.thoughtbot.expandablerecyclerview.models.ExpandableGroup;
|
||||||
|
import com.thoughtbot.expandablerecyclerview.viewholders.ChildViewHolder;
|
||||||
|
import com.thoughtbot.expandablerecyclerview.viewholders.GroupViewHolder;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.components.ExpandableViewHolder;
|
||||||
|
import com.topjohnwu.magisk.superuser.SuLogEntry;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
|
public class SuLogAdapter {
|
||||||
|
|
||||||
|
private ExpandableAdapter adapter;
|
||||||
|
private Set<SuLogEntry> expandList = new HashSet<>();
|
||||||
|
|
||||||
|
public SuLogAdapter(List<SuLogEntry> list) {
|
||||||
|
|
||||||
|
// Separate the logs with date
|
||||||
|
Map<String, List<SuLogEntry>> logEntryMap = new LinkedHashMap<>();
|
||||||
|
List<SuLogEntry> group;
|
||||||
|
for (SuLogEntry log : list) {
|
||||||
|
String date = log.getDateString();
|
||||||
|
group = logEntryMap.get(date);
|
||||||
|
if (group == null) {
|
||||||
|
group = new ArrayList<>();
|
||||||
|
logEntryMap.put(date, group);
|
||||||
|
}
|
||||||
|
group.add(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then format them into expandable groups
|
||||||
|
List<LogGroup> logEntryGroups = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<SuLogEntry>> entry : logEntryMap.entrySet()) {
|
||||||
|
logEntryGroups.add(new LogGroup(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
adapter = new ExpandableAdapter(logEntryGroups);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecyclerView.Adapter getAdapter() {
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ExpandableAdapter
|
||||||
|
extends ExpandableRecyclerViewAdapter<LogGroupViewHolder, LogViewHolder> {
|
||||||
|
|
||||||
|
ExpandableAdapter(List<? extends ExpandableGroup> groups) {
|
||||||
|
super(groups);
|
||||||
|
expandableList.expandedGroupIndexes[0] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LogGroupViewHolder onCreateGroupViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog_group, parent, false);
|
||||||
|
return new LogGroupViewHolder(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LogViewHolder onCreateChildViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_sulog, parent, false);
|
||||||
|
return new LogViewHolder(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindChildViewHolder(LogViewHolder holder, int flatPosition, ExpandableGroup group, int childIndex) {
|
||||||
|
Context context = holder.itemView.getContext();
|
||||||
|
SuLogEntry logEntry = (SuLogEntry) group.getItems().get(childIndex);
|
||||||
|
holder.setExpanded(expandList.contains(logEntry));
|
||||||
|
holder.itemView.setOnClickListener(view -> {
|
||||||
|
if (holder.getExpanded()) {
|
||||||
|
holder.collapse();
|
||||||
|
expandList.remove(logEntry);
|
||||||
|
} else {
|
||||||
|
holder.expand();
|
||||||
|
expandList.add(logEntry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
holder.appName.setText(logEntry.appName);
|
||||||
|
holder.action.setText(context.getString(logEntry.action ? R.string.grant : R.string.deny));
|
||||||
|
holder.command.setText(logEntry.command);
|
||||||
|
holder.fromPid.setText(String.valueOf(logEntry.fromPid));
|
||||||
|
holder.toUid.setText(String.valueOf(logEntry.toUid));
|
||||||
|
holder.time.setText(logEntry.getTimeString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindGroupViewHolder(LogGroupViewHolder holder, int flatPosition, ExpandableGroup group) {
|
||||||
|
holder.date.setText(group.getTitle());
|
||||||
|
if (isGroupExpanded(flatPosition)) {
|
||||||
|
holder.expand();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LogGroup extends ExpandableGroup<SuLogEntry> {
|
||||||
|
LogGroup(String title, List<SuLogEntry> items) {
|
||||||
|
super(title, items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class LogGroupViewHolder extends GroupViewHolder {
|
||||||
|
|
||||||
|
@BindView(R.id.date) TextView date;
|
||||||
|
@BindView(R.id.arrow) ImageView arrow;
|
||||||
|
|
||||||
|
public LogGroupViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
ButterKnife.bind(this, itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void expand() {
|
||||||
|
RotateAnimation rotate =
|
||||||
|
new RotateAnimation(360, 180, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||||
|
rotate.setDuration(300);
|
||||||
|
rotate.setFillAfter(true);
|
||||||
|
arrow.setAnimation(rotate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void collapse() {
|
||||||
|
RotateAnimation rotate =
|
||||||
|
new RotateAnimation(180, 360, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||||
|
rotate.setDuration(300);
|
||||||
|
rotate.setFillAfter(true);
|
||||||
|
arrow.setAnimation(rotate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper class
|
||||||
|
static class LogViewHolder extends ChildViewHolder {
|
||||||
|
|
||||||
|
private InternalViewHolder expandableViewHolder;
|
||||||
|
|
||||||
|
@BindView(R.id.app_name) TextView appName;
|
||||||
|
@BindView(R.id.action) TextView action;
|
||||||
|
@BindView(R.id.time) TextView time;
|
||||||
|
@BindView(R.id.fromPid) TextView fromPid;
|
||||||
|
@BindView(R.id.toUid) TextView toUid;
|
||||||
|
@BindView(R.id.command) TextView command;
|
||||||
|
|
||||||
|
LogViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
ButterKnife.bind(this, itemView);
|
||||||
|
expandableViewHolder = new InternalViewHolder(itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InternalViewHolder extends ExpandableViewHolder {
|
||||||
|
|
||||||
|
InternalViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setExpandLayout(View itemView) {
|
||||||
|
expandLayout = itemView.findViewById(R.id.expand_layout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getExpanded() {
|
||||||
|
return expandableViewHolder.mExpanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setExpanded(boolean expanded) {
|
||||||
|
expandableViewHolder.setExpanded(expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expand() {
|
||||||
|
expandableViewHolder.expand();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void collapse() {
|
||||||
|
expandableViewHolder.collapse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.topjohnwu.magisk.adapters;
|
||||||
|
|
||||||
|
|
||||||
|
import android.support.v4.app.Fragment;
|
||||||
|
import android.support.v4.app.FragmentManager;
|
||||||
|
import android.support.v4.app.FragmentPagerAdapter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TabFragmentAdapter extends FragmentPagerAdapter {
|
||||||
|
|
||||||
|
private List<Fragment> fragmentList;
|
||||||
|
private List<String> titleList;
|
||||||
|
|
||||||
|
public TabFragmentAdapter(FragmentManager fm) {
|
||||||
|
super(fm);
|
||||||
|
fragmentList = new ArrayList<>();
|
||||||
|
titleList = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Fragment getItem(int position) {
|
||||||
|
return fragmentList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return fragmentList.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getPageTitle(int position) {
|
||||||
|
return titleList.get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addTab(Fragment fragment, String title) {
|
||||||
|
fragmentList.add(fragment);
|
||||||
|
titleList.add(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.BuildConfig;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
import com.topjohnwu.magisk.utils.WebService;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
public class CheckUpdates extends ParallelTask<Void, Void, Void> {
|
||||||
|
|
||||||
|
private static final String UPDATE_JSON = "https://raw.githubusercontent.com/topjohnwu/MagiskManager/update/magisk_update.json";
|
||||||
|
|
||||||
|
private boolean showNotification = false;
|
||||||
|
|
||||||
|
public CheckUpdates(Context context, boolean b) {
|
||||||
|
this(context);
|
||||||
|
showNotification = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CheckUpdates(Context context) {
|
||||||
|
magiskManager = Utils.getMagiskManager(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... voids) {
|
||||||
|
String jsonStr = WebService.request(UPDATE_JSON, WebService.GET);
|
||||||
|
try {
|
||||||
|
JSONObject json = new JSONObject(jsonStr);
|
||||||
|
JSONObject magisk = json.getJSONObject("magisk");
|
||||||
|
magiskManager.remoteMagiskVersionString = magisk.getString("version");
|
||||||
|
magiskManager.remoteMagiskVersionCode = magisk.getInt("versionCode");
|
||||||
|
magiskManager.magiskLink = magisk.getString("link");
|
||||||
|
magiskManager.releaseNoteLink = magisk.getString("note");
|
||||||
|
JSONObject manager = json.getJSONObject("app");
|
||||||
|
magiskManager.remoteManagerVersionString = manager.getString("version");
|
||||||
|
magiskManager.remoteManagerVersionCode = manager.getInt("versionCode");
|
||||||
|
magiskManager.managerLink = manager.getString("link");
|
||||||
|
} catch (JSONException ignored) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
if (showNotification && magiskManager.updateNotification) {
|
||||||
|
if (BuildConfig.VERSION_CODE < magiskManager.remoteManagerVersionCode) {
|
||||||
|
Utils.showManagerUpdate(magiskManager);
|
||||||
|
} else if (magiskManager.magiskVersionCode < magiskManager.remoteMagiskVersionCode) {
|
||||||
|
Utils.showMagiskUpdate(magiskManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
magiskManager.updateCheckDone.trigger();
|
||||||
|
super.onPostExecute(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
154
app/src/main/java/com/topjohnwu/magisk/asyncs/FlashZip.java
Normal file
154
app/src/main/java/com/topjohnwu/magisk/asyncs/FlashZip.java
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.components.AlertDialogBuilder;
|
||||||
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
import com.topjohnwu.magisk.utils.ZipUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class FlashZip extends RootTask<Void, String, Integer> {
|
||||||
|
|
||||||
|
private Uri mUri;
|
||||||
|
private File mCachedFile, mScriptFile, mCheckFile;
|
||||||
|
|
||||||
|
private String mFilename;
|
||||||
|
private ProgressDialog progress;
|
||||||
|
|
||||||
|
public FlashZip(Activity context, Uri uri) {
|
||||||
|
super(context);
|
||||||
|
mUri = uri;
|
||||||
|
|
||||||
|
mCachedFile = new File(magiskManager.getCacheDir(), "install.zip");
|
||||||
|
mScriptFile = new File(magiskManager.getCacheDir(), "/META-INF/com/google/android/update-binary");
|
||||||
|
mCheckFile = new File(mScriptFile.getParent(), "updater-script");
|
||||||
|
|
||||||
|
// Try to get the filename ourselves
|
||||||
|
mFilename = Utils.getNameFromUri(magiskManager, mUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyToCache() throws Throwable {
|
||||||
|
publishProgress(magiskManager.getString(R.string.copying_msg));
|
||||||
|
|
||||||
|
if (mCachedFile.exists() && !mCachedFile.delete()) {
|
||||||
|
Logger.error("FlashZip: Error while deleting already existing file");
|
||||||
|
throw new IOException();
|
||||||
|
}
|
||||||
|
try (
|
||||||
|
InputStream in = magiskManager.getContentResolver().openInputStream(mUri);
|
||||||
|
OutputStream outputStream = new FileOutputStream(mCachedFile)
|
||||||
|
) {
|
||||||
|
byte buffer[] = new byte[1024];
|
||||||
|
int length;
|
||||||
|
if (in == null) throw new FileNotFoundException();
|
||||||
|
while ((length = in.read(buffer)) > 0)
|
||||||
|
outputStream.write(buffer, 0, length);
|
||||||
|
|
||||||
|
Logger.dev("FlashZip: File created successfully - " + mCachedFile.getPath());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Logger.error("FlashZip: Invalid Uri");
|
||||||
|
throw e;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Logger.error("FlashZip: Error in creating file");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean unzipAndCheck() throws Exception {
|
||||||
|
ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android");
|
||||||
|
List<String> ret;
|
||||||
|
ret = Utils.readFile(mCheckFile.getPath());
|
||||||
|
return Utils.isValidShellResponse(ret) && ret.get(0).contains("#MAGISK");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int cleanup(int ret) {
|
||||||
|
Shell.su(
|
||||||
|
"rm -rf " + mCachedFile.getParent() + "/*",
|
||||||
|
"rm -rf " + MagiskManager.TMP_FOLDER_PATH
|
||||||
|
);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
progress = new ProgressDialog(activity);
|
||||||
|
progress.setTitle(R.string.zip_install_progress_title);
|
||||||
|
progress.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(String... values) {
|
||||||
|
progress.setMessage(values[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Integer doInRoot(Void... voids) {
|
||||||
|
Logger.dev("FlashZip Running... " + mFilename);
|
||||||
|
List<String> ret;
|
||||||
|
try {
|
||||||
|
copyToCache();
|
||||||
|
if (!unzipAndCheck()) return cleanup(0);
|
||||||
|
publishProgress(magiskManager.getString(R.string.zip_install_progress_msg, mFilename));
|
||||||
|
ret = Shell.su(
|
||||||
|
"BOOTMODE=true sh " + mScriptFile + " dummy 1 " + mCachedFile,
|
||||||
|
"if [ $? -eq 0 ]; then echo true; else echo false; fi"
|
||||||
|
);
|
||||||
|
if (!Utils.isValidShellResponse(ret)) return -1;
|
||||||
|
Logger.dev("FlashZip: Console log:");
|
||||||
|
for (String line : ret) {
|
||||||
|
Logger.dev(line);
|
||||||
|
}
|
||||||
|
if (Boolean.parseBoolean(ret.get(ret.size() - 1)))
|
||||||
|
return cleanup(1);
|
||||||
|
|
||||||
|
} catch (Throwable e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return cleanup(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -1 = error, manual install; 0 = invalid zip; 1 = success
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Integer result) {
|
||||||
|
progress.dismiss();
|
||||||
|
switch (result) {
|
||||||
|
case -1:
|
||||||
|
Toast.makeText(magiskManager, magiskManager.getString(R.string.install_error), Toast.LENGTH_LONG).show();
|
||||||
|
Utils.showUriSnack(activity, mUri);
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
Toast.makeText(magiskManager, magiskManager.getString(R.string.invalid_zip), Toast.LENGTH_LONG).show();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
onSuccess();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
super.onPostExecute(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onSuccess() {
|
||||||
|
magiskManager.updateCheckDone.trigger();
|
||||||
|
new LoadModules(activity).exec();
|
||||||
|
|
||||||
|
new AlertDialogBuilder(activity)
|
||||||
|
.setTitle(R.string.reboot_title)
|
||||||
|
.setMessage(R.string.reboot_msg)
|
||||||
|
.setPositiveButton(R.string.reboot, (dialogInterface, i) -> Shell.su(true, "reboot"))
|
||||||
|
.setNegativeButton(R.string.no_thanks, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
public class GetBootBlocks extends RootTask<Void, Void, Void> {
|
||||||
|
|
||||||
|
public GetBootBlocks(Activity context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInRoot(Void... params) {
|
||||||
|
magiskManager.blockList = Shell.su(
|
||||||
|
"find /dev/block -type b -maxdepth 1 | grep -v -E \"loop|ram|dm-0\""
|
||||||
|
);
|
||||||
|
if (magiskManager.bootBlock == null) {
|
||||||
|
magiskManager.bootBlock = Utils.detectBootImage();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
magiskManager.blockDetectionDone.trigger();
|
||||||
|
super.onPostExecute(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/src/main/java/com/topjohnwu/magisk/asyncs/LoadApps.java
Normal file
39
app/src/main/java/com/topjohnwu/magisk/asyncs/LoadApps.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.adapters.ApplicationAdapter;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LoadApps extends ParallelTask<Void, Void, Void> {
|
||||||
|
|
||||||
|
public LoadApps(Activity context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... voids) {
|
||||||
|
PackageManager pm = magiskManager.getPackageManager();
|
||||||
|
List<ApplicationInfo> list = pm.getInstalledApplications(0);
|
||||||
|
for (Iterator<ApplicationInfo> i = list.iterator(); i.hasNext(); ) {
|
||||||
|
ApplicationInfo info = i.next();
|
||||||
|
if (ApplicationAdapter.BLACKLIST.contains(info.packageName) || !info.enabled) {
|
||||||
|
i.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(list, (a, b) -> a.loadLabel(pm).toString().toLowerCase()
|
||||||
|
.compareTo(b.loadLabel(pm).toString().toLowerCase()));
|
||||||
|
magiskManager.appList = Collections.unmodifiableList(list);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
new MagiskHide(activity).list();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.module.BaseModule;
|
||||||
|
import com.topjohnwu.magisk.module.Module;
|
||||||
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
import com.topjohnwu.magisk.utils.ValueSortedMap;
|
||||||
|
|
||||||
|
public class LoadModules extends RootTask<Void, Void, Void> {
|
||||||
|
|
||||||
|
public LoadModules(Activity context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInRoot(Void... voids) {
|
||||||
|
Logger.dev("LoadModules: Loading modules");
|
||||||
|
|
||||||
|
magiskManager.moduleMap = new ValueSortedMap<>();
|
||||||
|
|
||||||
|
for (String path : Utils.getModList(MagiskManager.MAGISK_PATH)) {
|
||||||
|
Logger.dev("LoadModules: Adding modules from " + path);
|
||||||
|
Module module;
|
||||||
|
try {
|
||||||
|
module = new Module(path);
|
||||||
|
magiskManager.moduleMap.put(module.getId(), module);
|
||||||
|
} catch (BaseModule.CacheModException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.dev("LoadModules: Data load done");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
magiskManager.moduleLoadDone.trigger();
|
||||||
|
super.onPostExecute(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
189
app/src/main/java/com/topjohnwu/magisk/asyncs/LoadRepos.java
Normal file
189
app/src/main/java/com/topjohnwu/magisk/asyncs/LoadRepos.java
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
||||||
|
import com.topjohnwu.magisk.module.BaseModule;
|
||||||
|
import com.topjohnwu.magisk.module.Repo;
|
||||||
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
|
import com.topjohnwu.magisk.utils.ValueSortedMap;
|
||||||
|
import com.topjohnwu.magisk.utils.WebService;
|
||||||
|
|
||||||
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class LoadRepos extends ParallelTask<Void, Void, Void> {
|
||||||
|
|
||||||
|
public static final String ETAG_KEY = "ETag";
|
||||||
|
|
||||||
|
private static final String REPO_URL = "https://api.github.com/users/Magisk-Modules-Repo/repos?per_page=100&page=%d";
|
||||||
|
private static final String IF_NONE_MATCH = "If-None-Match";
|
||||||
|
private static final String LINK_KEY = "Link";
|
||||||
|
|
||||||
|
private static final int CHECK_ETAG = 0;
|
||||||
|
private static final int LOAD_NEXT = 1;
|
||||||
|
private static final int LOAD_PREV = 2;
|
||||||
|
|
||||||
|
private List<String> etags;
|
||||||
|
private ValueSortedMap<String, Repo> cached, fetched;
|
||||||
|
private RepoDatabaseHelper repoDB;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
public LoadRepos(Activity context) {
|
||||||
|
super(context);
|
||||||
|
prefs = magiskManager.prefs;
|
||||||
|
String prefsPath = context.getApplicationInfo().dataDir + "/shared_prefs";
|
||||||
|
repoDB = new RepoDatabaseHelper(magiskManager);
|
||||||
|
// Legacy data cleanup
|
||||||
|
File old = new File(prefsPath, "RepoMap.xml");
|
||||||
|
if (old.exists() || !prefs.getString("repomap", "empty").equals("empty")) {
|
||||||
|
old.delete();
|
||||||
|
prefs.edit().remove("version").remove("repomap").remove(ETAG_KEY).apply();
|
||||||
|
repoDB.clearRepo();
|
||||||
|
}
|
||||||
|
etags = new ArrayList<>(
|
||||||
|
Arrays.asList(magiskManager.prefs.getString(ETAG_KEY, "").split(",")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadJSON(String jsonString) throws Exception {
|
||||||
|
JSONArray jsonArray = new JSONArray(jsonString);
|
||||||
|
|
||||||
|
for (int i = 0; i < jsonArray.length(); i++) {
|
||||||
|
JSONObject jsonobject = jsonArray.getJSONObject(i);
|
||||||
|
String id = jsonobject.getString("description");
|
||||||
|
String name = jsonobject.getString("name");
|
||||||
|
String lastUpdate = jsonobject.getString("pushed_at");
|
||||||
|
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||||
|
Date updatedDate = format.parse(lastUpdate);
|
||||||
|
Repo repo = cached.get(id);
|
||||||
|
try {
|
||||||
|
if (repo == null) {
|
||||||
|
Logger.dev("LoadRepos: Create new repo " + id);
|
||||||
|
repo = new Repo(name, updatedDate);
|
||||||
|
} else {
|
||||||
|
// Popout from cached
|
||||||
|
cached.remove(id);
|
||||||
|
repo.update(updatedDate);
|
||||||
|
}
|
||||||
|
if (repo.getId() != null) {
|
||||||
|
fetched.put(id, repo);
|
||||||
|
}
|
||||||
|
} catch (BaseModule.CacheModException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean loadPage(int page, String url, int mode) {
|
||||||
|
Logger.dev("LoadRepos: Loading page: " + (page + 1));
|
||||||
|
Map<String, String> header = new HashMap<>();
|
||||||
|
if (mode == CHECK_ETAG && page < etags.size() && !TextUtils.isEmpty(etags.get(page))) {
|
||||||
|
Logger.dev("ETAG: " + etags.get(page));
|
||||||
|
header.put(IF_NONE_MATCH, etags.get(page));
|
||||||
|
}
|
||||||
|
if (url == null) {
|
||||||
|
url = String.format(Locale.US, REPO_URL, page + 1);
|
||||||
|
}
|
||||||
|
String jsonString = WebService.request(url, WebService.GET, header, true);
|
||||||
|
if (TextUtils.isEmpty(jsonString)) {
|
||||||
|
// At least check the pages we know
|
||||||
|
return page + 1 < etags.size() && loadPage(page + 1, null, CHECK_ETAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request succeed, parse the new stuffs
|
||||||
|
try {
|
||||||
|
loadJSON(jsonString);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the ETAG
|
||||||
|
String newEtag = header.get(ETAG_KEY);
|
||||||
|
newEtag = newEtag.substring(newEtag.indexOf('\"'), newEtag.lastIndexOf('\"') + 1);
|
||||||
|
Logger.dev("New ETAG: " + newEtag);
|
||||||
|
if (page < etags.size()) {
|
||||||
|
etags.set(page, newEtag);
|
||||||
|
} else {
|
||||||
|
etags.add(newEtag);
|
||||||
|
}
|
||||||
|
|
||||||
|
String links = header.get(LINK_KEY);
|
||||||
|
if (links != null) {
|
||||||
|
if (mode == CHECK_ETAG || mode == LOAD_NEXT) {
|
||||||
|
// Try to check next page URL
|
||||||
|
url = null;
|
||||||
|
for (String s : links.split(", ")) {
|
||||||
|
if (s.contains("next")) {
|
||||||
|
url = s.substring(s.indexOf("<") + 1, s.indexOf(">; "));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url != null) {
|
||||||
|
loadPage(page + 1, url, LOAD_NEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == CHECK_ETAG || mode == LOAD_PREV) {
|
||||||
|
// Try to check prev page URL
|
||||||
|
url = null;
|
||||||
|
for (String s : links.split(", ")) {
|
||||||
|
if (s.contains("prev")) {
|
||||||
|
url = s.substring(s.indexOf("<") + 1, s.indexOf(">; "));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url != null) {
|
||||||
|
loadPage(page - 1, url, LOAD_PREV);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... voids) {
|
||||||
|
Logger.dev("LoadRepos: Loading repos");
|
||||||
|
|
||||||
|
cached = repoDB.getRepoMap(false);
|
||||||
|
fetched = new ValueSortedMap<>();
|
||||||
|
|
||||||
|
if (!loadPage(0, null, CHECK_ETAG)) {
|
||||||
|
magiskManager.repoMap = repoDB.getRepoMap();
|
||||||
|
Logger.dev("LoadRepos: No updates, use DB");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
repoDB.addRepoMap(fetched);
|
||||||
|
repoDB.removeRepo(cached);
|
||||||
|
|
||||||
|
// Update ETag
|
||||||
|
StringBuilder etagBuilder = new StringBuilder();
|
||||||
|
for (int i = 0; i < etags.size(); ++i) {
|
||||||
|
if (i != 0) etagBuilder.append(",");
|
||||||
|
etagBuilder.append(etags.get(i));
|
||||||
|
}
|
||||||
|
prefs.edit().putString(ETAG_KEY, etagBuilder.toString()).apply();
|
||||||
|
|
||||||
|
magiskManager.repoMap = repoDB.getRepoMap();
|
||||||
|
Logger.dev("LoadRepos: Done");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
magiskManager.repoLoadDone.trigger();
|
||||||
|
super.onPostExecute(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MagiskHide extends RootTask<Object, Void, Void> {
|
||||||
|
|
||||||
|
private boolean isList = false;
|
||||||
|
|
||||||
|
public MagiskHide() {}
|
||||||
|
|
||||||
|
public MagiskHide(Activity context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Void doInRoot(Object... params) {
|
||||||
|
String command = (String) params[0];
|
||||||
|
List<String> ret = Shell.su("magiskhide --" + command);
|
||||||
|
if (isList) {
|
||||||
|
magiskManager.magiskHideList = ret;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
if (isList) {
|
||||||
|
magiskManager.magiskHideDone.trigger();
|
||||||
|
}
|
||||||
|
super.onPostExecute(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(CharSequence packageName) {
|
||||||
|
exec("add " + packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rm(CharSequence packageName) {
|
||||||
|
exec("rm " + packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enable() {
|
||||||
|
exec("enable");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disable() {
|
||||||
|
exec("disable");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void list() {
|
||||||
|
isList = true;
|
||||||
|
if (magiskManager == null) return;
|
||||||
|
exec("ls");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
public abstract class ParallelTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
||||||
|
|
||||||
|
protected Activity activity;
|
||||||
|
protected MagiskManager magiskManager;
|
||||||
|
|
||||||
|
private Runnable callback = null;
|
||||||
|
|
||||||
|
public ParallelTask() {}
|
||||||
|
|
||||||
|
public ParallelTask(Activity context) {
|
||||||
|
activity = context;
|
||||||
|
magiskManager = Utils.getMagiskManager(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public void exec(Params... params) {
|
||||||
|
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Result result) {
|
||||||
|
if (callback != null) callback.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParallelTask<Params, Progress, Result> setCallBack(Runnable next) {
|
||||||
|
callback = next;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
public class ProcessMagiskZip extends ParallelTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
private Uri mUri;
|
||||||
|
private ProgressDialog progressDialog;
|
||||||
|
private String mBoot;
|
||||||
|
private boolean mEnc, mVerity;
|
||||||
|
|
||||||
|
public ProcessMagiskZip(Activity context, Uri uri, String boot, boolean enc, boolean verity) {
|
||||||
|
super(context);
|
||||||
|
mUri = uri;
|
||||||
|
mBoot = boot;
|
||||||
|
mEnc = enc;
|
||||||
|
mVerity = verity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
progressDialog = ProgressDialog.show(activity,
|
||||||
|
activity.getString(R.string.zip_process_title),
|
||||||
|
activity.getString(R.string.zip_unzip_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
if (Shell.rootAccess()) {
|
||||||
|
synchronized (Shell.lock) {
|
||||||
|
Shell.su("rm -f /dev/.magisk",
|
||||||
|
(mBoot != null) ? "echo \"BOOTIMAGE=" + mBoot + "\" >> /dev/.magisk" : "",
|
||||||
|
"echo \"KEEPFORCEENCRYPT=" + String.valueOf(mEnc) + "\" >> /dev/.magisk",
|
||||||
|
"echo \"KEEPVERITY=" + String.valueOf(mVerity) + "\" >> /dev/.magisk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
progressDialog.dismiss();
|
||||||
|
if (result) {
|
||||||
|
new FlashZip(activity, mUri).exec();
|
||||||
|
} else {
|
||||||
|
Utils.showUriSnack(activity, mUri);
|
||||||
|
}
|
||||||
|
super.onPostExecute(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
import com.topjohnwu.magisk.utils.ZipUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class ProcessRepoZip extends ParallelTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
private Uri mUri;
|
||||||
|
private ProgressDialog progressDialog;
|
||||||
|
private boolean mInstall;
|
||||||
|
|
||||||
|
public ProcessRepoZip(Activity context, Uri uri, boolean install) {
|
||||||
|
super(context);
|
||||||
|
mUri = uri;
|
||||||
|
mInstall = install;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
progressDialog = ProgressDialog.show(activity,
|
||||||
|
activity.getString(R.string.zip_process_title),
|
||||||
|
activity.getString(R.string.zip_process_msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Create temp file
|
||||||
|
File temp1 = new File(magiskManager.getCacheDir(), "1.zip");
|
||||||
|
File temp2 = new File(magiskManager.getCacheDir(), "2.zip");
|
||||||
|
magiskManager.getCacheDir().mkdirs();
|
||||||
|
temp1.createNewFile();
|
||||||
|
temp2.createNewFile();
|
||||||
|
|
||||||
|
// First remove top folder in Github source zip, Uri -> temp1
|
||||||
|
ZipUtils.removeTopFolder(activity.getContentResolver().openInputStream(mUri), temp1);
|
||||||
|
|
||||||
|
// Then sign the zip for the first time, temp1 -> temp2
|
||||||
|
ZipUtils.signZip(activity, temp1, temp2, false);
|
||||||
|
|
||||||
|
// Adjust the zip to prevent unzip issues, temp2 -> temp1
|
||||||
|
ZipUtils.zipAdjust(temp2.getPath(), temp1.getPath());
|
||||||
|
|
||||||
|
// Finally, sign the whole zip file again, temp1 -> temp2
|
||||||
|
ZipUtils.signZip(activity, temp1, temp2, true);
|
||||||
|
|
||||||
|
// Write it back to the downloaded zip, temp2 -> Uri
|
||||||
|
FileInputStream in = new FileInputStream(temp2);
|
||||||
|
try (OutputStream target = activity.getContentResolver().openOutputStream(mUri)) {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int length;
|
||||||
|
if (target == null) throw new FileNotFoundException();
|
||||||
|
while ((length = in.read(buffer)) > 0)
|
||||||
|
target.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the temp file
|
||||||
|
temp1.delete();
|
||||||
|
temp2.delete();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.error("ProcessRepoZip: Error!");
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
progressDialog.dismiss();
|
||||||
|
if (result) {
|
||||||
|
if (Shell.rootAccess() && mInstall) {
|
||||||
|
new FlashZip(activity, mUri).exec();
|
||||||
|
} else {
|
||||||
|
Utils.showUriSnack(activity, mUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
Toast.makeText(activity, R.string.process_error, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/src/main/java/com/topjohnwu/magisk/asyncs/RootTask.java
Normal file
33
app/src/main/java/com/topjohnwu/magisk/asyncs/RootTask.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package com.topjohnwu.magisk.asyncs;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.utils.Shell;
|
||||||
|
|
||||||
|
public abstract class RootTask <Params, Progress, Result> extends ParallelTask<Params, Progress, Result> {
|
||||||
|
|
||||||
|
public RootTask() {}
|
||||||
|
|
||||||
|
public RootTask(Activity context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
@Override
|
||||||
|
final protected Result doInBackground(Params... params) {
|
||||||
|
synchronized (Shell.lock) {
|
||||||
|
return doInRoot(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
abstract protected Result doInRoot(Params... params);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public void exec(Params... params) {
|
||||||
|
if (Shell.rootAccess()) {
|
||||||
|
super.exec(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.topjohnwu.magisk;
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
@@ -26,6 +26,8 @@ import android.widget.ImageView;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author dvdandroid
|
* @author dvdandroid
|
||||||
*/
|
*/
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
|
public class Activity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MagiskManager getApplicationContext() {
|
||||||
|
return (MagiskManager) super.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setFloating() {
|
||||||
|
boolean isTablet = getResources().getBoolean(R.bool.isTablet);
|
||||||
|
if (isTablet) {
|
||||||
|
WindowManager.LayoutParams params = getWindow().getAttributes();
|
||||||
|
params.height = getResources().getDimensionPixelSize(R.dimen.floating_height);
|
||||||
|
params.width = getResources().getDimensionPixelSize(R.dimen.floating_width);
|
||||||
|
params.alpha = 1.0f;
|
||||||
|
params.dimAmount = 0.6f;
|
||||||
|
params.flags |= 2;
|
||||||
|
getWindow().setAttributes(params);
|
||||||
|
setFinishOnTouchOutside(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.annotation.StyleRes;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewStub;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
|
public class AlertDialogBuilder extends AlertDialog.Builder {
|
||||||
|
|
||||||
|
@BindView(R.id.button_panel) LinearLayout buttons;
|
||||||
|
@BindView(R.id.message_panel) LinearLayout messagePanel;
|
||||||
|
|
||||||
|
@BindView(R.id.negative) Button negative;
|
||||||
|
@BindView(R.id.positive) Button positive;
|
||||||
|
@BindView(R.id.neutral) Button neutral;
|
||||||
|
@BindView(R.id.message) TextView messageView;
|
||||||
|
@BindView(R.id.custom_view) ViewStub custom;
|
||||||
|
|
||||||
|
private DialogInterface.OnClickListener positiveListener;
|
||||||
|
private DialogInterface.OnClickListener negativeListener;
|
||||||
|
private DialogInterface.OnClickListener neutralListener;
|
||||||
|
|
||||||
|
private AlertDialog dialog;
|
||||||
|
|
||||||
|
public AlertDialogBuilder(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertDialogBuilder(@NonNull Context context, @StyleRes int themeResId) {
|
||||||
|
super(context, themeResId);
|
||||||
|
setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setup() {
|
||||||
|
View v = LayoutInflater.from(getContext()).inflate(R.layout.alert_dialog, null);
|
||||||
|
ButterKnife.bind(this, v);
|
||||||
|
super.setView(v);
|
||||||
|
negative.setVisibility(View.GONE);
|
||||||
|
positive.setVisibility(View.GONE);
|
||||||
|
neutral.setVisibility(View.GONE);
|
||||||
|
buttons.setVisibility(View.GONE);
|
||||||
|
messagePanel.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setView(int layoutResId) {
|
||||||
|
custom.setLayoutResource(layoutResId);
|
||||||
|
custom.inflate();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setView(View view) {
|
||||||
|
ViewGroup parent = (ViewGroup) custom.getParent();
|
||||||
|
int idx = parent.indexOfChild(custom);
|
||||||
|
parent.removeView(custom);
|
||||||
|
parent.addView(view, idx);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setMessage(@Nullable CharSequence message) {
|
||||||
|
messageView.setText(message);
|
||||||
|
messagePanel.setVisibility(View.VISIBLE);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setMessage(@StringRes int messageId) {
|
||||||
|
return setMessage(getContext().getString(messageId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setPositiveButton(CharSequence text, DialogInterface.OnClickListener listener) {
|
||||||
|
buttons.setVisibility(View.VISIBLE);
|
||||||
|
positive.setVisibility(View.VISIBLE);
|
||||||
|
positive.setText(text);
|
||||||
|
positiveListener = listener;
|
||||||
|
positive.setOnClickListener((v) -> {
|
||||||
|
if (positiveListener != null) {
|
||||||
|
positiveListener.onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setPositiveButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
|
||||||
|
return setPositiveButton(getContext().getString(textId), listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setNegativeButton(CharSequence text, DialogInterface.OnClickListener listener) {
|
||||||
|
buttons.setVisibility(View.VISIBLE);
|
||||||
|
negative.setVisibility(View.VISIBLE);
|
||||||
|
negative.setText(text);
|
||||||
|
negativeListener = listener;
|
||||||
|
negative.setOnClickListener((v) -> {
|
||||||
|
if (negativeListener != null) {
|
||||||
|
negativeListener.onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setNegativeButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
|
||||||
|
return setNegativeButton(getContext().getString(textId), listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setNeutralButton(CharSequence text, DialogInterface.OnClickListener listener) {
|
||||||
|
buttons.setVisibility(View.VISIBLE);
|
||||||
|
neutral.setVisibility(View.VISIBLE);
|
||||||
|
neutral.setText(text);
|
||||||
|
neutralListener = listener;
|
||||||
|
neutral.setOnClickListener((v) -> {
|
||||||
|
if (neutralListener != null) {
|
||||||
|
neutralListener.onClick(dialog, DialogInterface.BUTTON_NEUTRAL);
|
||||||
|
}
|
||||||
|
dialog.dismiss();
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog.Builder setNeutralButton(@StringRes int textId, DialogInterface.OnClickListener listener) {
|
||||||
|
return setNeutralButton(getContext().getString(textId), listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog create() {
|
||||||
|
dialog = super.create();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AlertDialog show() {
|
||||||
|
create();
|
||||||
|
dialog.show();
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
|
||||||
|
public abstract class ExpandableViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
protected ViewGroup expandLayout;
|
||||||
|
private ValueAnimator expandAnimator, collapseAnimator;
|
||||||
|
private static int expandHeight = 0;
|
||||||
|
|
||||||
|
public boolean mExpanded = false;
|
||||||
|
|
||||||
|
public ExpandableViewHolder(View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
setExpandLayout(itemView);
|
||||||
|
expandLayout.getViewTreeObserver().addOnPreDrawListener(
|
||||||
|
new ViewTreeObserver.OnPreDrawListener() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onPreDraw() {
|
||||||
|
if (expandHeight == 0) {
|
||||||
|
final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
expandLayout.measure(widthSpec, heightSpec);
|
||||||
|
expandHeight = expandLayout.getMeasuredHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
expandLayout.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
|
expandLayout.setVisibility(View.GONE);
|
||||||
|
expandAnimator = slideAnimator(0, expandHeight);
|
||||||
|
collapseAnimator = slideAnimator(expandHeight, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExpanded(boolean expanded) {
|
||||||
|
mExpanded = expanded;
|
||||||
|
ViewGroup.LayoutParams layoutParams = expandLayout.getLayoutParams();
|
||||||
|
layoutParams.height = expanded ? expandHeight : 0;
|
||||||
|
expandLayout.setLayoutParams(layoutParams);
|
||||||
|
expandLayout.setVisibility(expanded ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void expand() {
|
||||||
|
if (mExpanded) return;
|
||||||
|
expandLayout.setVisibility(View.VISIBLE);
|
||||||
|
expandAnimator.start();
|
||||||
|
mExpanded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void collapse() {
|
||||||
|
if (!mExpanded) return;
|
||||||
|
collapseAnimator.start();
|
||||||
|
mExpanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void setExpandLayout(View itemView);
|
||||||
|
|
||||||
|
private ValueAnimator slideAnimator(int start, int end) {
|
||||||
|
|
||||||
|
ValueAnimator animator = ValueAnimator.ofInt(start, end);
|
||||||
|
|
||||||
|
animator.addUpdateListener(valueAnimator -> {
|
||||||
|
int value = (Integer) valueAnimator.getAnimatedValue();
|
||||||
|
ViewGroup.LayoutParams layoutParams = expandLayout.getLayoutParams();
|
||||||
|
layoutParams.height = value;
|
||||||
|
expandLayout.setLayoutParams(layoutParams);
|
||||||
|
});
|
||||||
|
return animator;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
public class Fragment extends android.support.v4.app.Fragment {
|
||||||
|
|
||||||
|
public MagiskManager getApplication() {
|
||||||
|
return Utils.getMagiskManager(getActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import us.feras.mdv.MarkdownView;
|
||||||
|
|
||||||
|
public class MarkDownWindow {
|
||||||
|
|
||||||
|
public MarkDownWindow(String title, String url, Context context) {
|
||||||
|
MagiskManager magiskManager = Utils.getMagiskManager(context);
|
||||||
|
AlertDialog.Builder alert = new AlertDialog.Builder(context);
|
||||||
|
alert.setTitle(title);
|
||||||
|
|
||||||
|
Logger.dev("WebView: URL = " + url);
|
||||||
|
|
||||||
|
MarkdownView md = new MarkdownView(context);
|
||||||
|
md.loadMarkdownFile(url, "file:///android_asset/" +
|
||||||
|
(magiskManager.isDarkTheme ? "dark" : "light") + ".css");
|
||||||
|
|
||||||
|
alert.setView(md);
|
||||||
|
alert.setNegativeButton(R.string.close, (dialog, id) -> dialog.dismiss());
|
||||||
|
alert.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.topjohnwu.magisk.components;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
|
public class SnackbarMaker {
|
||||||
|
|
||||||
|
public static Snackbar make(Activity activity, CharSequence text, int duration) {
|
||||||
|
View view = activity.findViewById(android.R.id.content);
|
||||||
|
return make(view, text, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar make(Activity activity, @StringRes int resId, int duration) {
|
||||||
|
return make(activity, activity.getString(resId), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar make(View view, CharSequence text, int duration) {
|
||||||
|
Snackbar snack = Snackbar.make(view, text, duration);
|
||||||
|
setup(snack);
|
||||||
|
return snack;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Snackbar make(View view, @StringRes int resId, int duration) {
|
||||||
|
Snackbar snack = Snackbar.make(view, resId, duration);
|
||||||
|
setup(snack);
|
||||||
|
return snack;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setup(Snackbar snack) {
|
||||||
|
TextView text = ButterKnife.findById(snack.getView(), android.support.design.R.id.snackbar_text);
|
||||||
|
text.setMaxLines(Integer.MAX_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.topjohnwu.magisk.database;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.module.Repo;
|
||||||
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
|
import com.topjohnwu.magisk.utils.ValueSortedMap;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
public class RepoDatabaseHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
|
private static final int DATABASE_VER = 2;
|
||||||
|
private static final String TABLE_NAME = "repos";
|
||||||
|
private static final int MIN_TEMPLATE_VER = 3;
|
||||||
|
|
||||||
|
public RepoDatabaseHelper(Context context) {
|
||||||
|
super(context, "repo.db", null, DATABASE_VER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
onUpgrade(db, 0, DATABASE_VER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
if (oldVersion == 0) {
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " " +
|
||||||
|
"(id TEXT, name TEXT, version TEXT, versionCode INT, " +
|
||||||
|
"author TEXT, description TEXT, repo_name TEXT, last_update INT, " +
|
||||||
|
"PRIMARY KEY(id))");
|
||||||
|
oldVersion++;
|
||||||
|
}
|
||||||
|
if (oldVersion == 1) {
|
||||||
|
db.execSQL("ALTER TABLE " + TABLE_NAME + " ADD template INT");
|
||||||
|
oldVersion++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addRepoMap(ValueSortedMap<String, Repo> map) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
Collection<Repo> list = map.values();
|
||||||
|
for (Repo repo : list) {
|
||||||
|
Logger.dev("Add to DB: " + repo.getId());
|
||||||
|
db.replace(TABLE_NAME, null, repo.getContentValues());
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearRepo() {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(TABLE_NAME, null, null);
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeRepo(ValueSortedMap<String, Repo> map) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
Collection<Repo> list = map.values();
|
||||||
|
for (Repo repo : list) {
|
||||||
|
Logger.dev("Remove from DB: " + repo.getId());
|
||||||
|
db.delete(TABLE_NAME, "id=?", new String[] { repo.getId() });
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueSortedMap<String, Repo> getRepoMap() {
|
||||||
|
return getRepoMap(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueSortedMap<String, Repo> getRepoMap(boolean filtered) {
|
||||||
|
ValueSortedMap<String, Repo> ret = new ValueSortedMap<>();
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
Repo repo;
|
||||||
|
try (Cursor c = db.query(TABLE_NAME, null, null, null, null, null, null)) {
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
repo = new Repo(c);
|
||||||
|
if (repo.getTemplateVersion() < MIN_TEMPLATE_VER && filtered) {
|
||||||
|
Logger.dev("Outdated repo: " + repo.getId());
|
||||||
|
} else {
|
||||||
|
// Logger.dev("Load from DB: " + repo.getId());
|
||||||
|
ret.put(repo.getId(), repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
package com.topjohnwu.magisk.database;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.database.sqlite.SQLiteOpenHelper;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.superuser.Policy;
|
||||||
|
import com.topjohnwu.magisk.superuser.SuLogEntry;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SuDatabaseHelper extends SQLiteOpenHelper {
|
||||||
|
|
||||||
|
public static final String ROOT_ACCESS = "root_access";
|
||||||
|
public static final String MULTIUSER_MODE = "multiuser_mode";
|
||||||
|
public static final String MNT_NS = "mnt_ns";
|
||||||
|
|
||||||
|
private static final int DATABASE_VER = 2;
|
||||||
|
private static final String POLICY_TABLE = "policies";
|
||||||
|
private static final String LOG_TABLE = "logs";
|
||||||
|
private static final String SETTINGS_TABLE = "settings";
|
||||||
|
|
||||||
|
private MagiskManager magiskManager;
|
||||||
|
private PackageManager pm;
|
||||||
|
|
||||||
|
public SuDatabaseHelper(Context context) {
|
||||||
|
super(context, "su.db", null, DATABASE_VER);
|
||||||
|
magiskManager = Utils.getMagiskManager(context);
|
||||||
|
pm = context.getPackageManager();
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(SQLiteDatabase db) {
|
||||||
|
onUpgrade(db, 0, DATABASE_VER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||||
|
if (oldVersion == 0) {
|
||||||
|
createTables(db);
|
||||||
|
oldVersion = 2;
|
||||||
|
}
|
||||||
|
if (oldVersion == 1) {
|
||||||
|
// We're dropping column app_name, rename and re-construct table
|
||||||
|
db.execSQL("ALTER TABLE " + POLICY_TABLE + " RENAME TO " + POLICY_TABLE + "_old");
|
||||||
|
|
||||||
|
// Create the new tables
|
||||||
|
createTables(db);
|
||||||
|
|
||||||
|
// Migrate old data to new tables
|
||||||
|
db.execSQL(
|
||||||
|
"INSERT INTO " + POLICY_TABLE + " SELECT " +
|
||||||
|
"uid, package_name, policy, until, logging, notification " +
|
||||||
|
"FROM " + POLICY_TABLE + "_old");
|
||||||
|
db.execSQL("DROP TABLE " + POLICY_TABLE + "_old");
|
||||||
|
|
||||||
|
File oldDB = magiskManager.getDatabasePath("sulog.db");
|
||||||
|
if (oldDB.exists()) {
|
||||||
|
migrateLegacyLogList(oldDB, db);
|
||||||
|
magiskManager.deleteDatabase("sulog.db");
|
||||||
|
}
|
||||||
|
++oldVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTables(SQLiteDatabase db) {
|
||||||
|
// Policies
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + POLICY_TABLE + " " +
|
||||||
|
"(uid INT, package_name TEXT, policy INT, " +
|
||||||
|
"until INT, logging INT, notification INT, " +
|
||||||
|
"PRIMARY KEY(uid))");
|
||||||
|
|
||||||
|
// Logs
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + LOG_TABLE + " " +
|
||||||
|
"(from_uid INT, package_name TEXT, app_name TEXT, from_pid INT, " +
|
||||||
|
"to_uid INT, action INT, time INT, command TEXT)");
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
db.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS " + SETTINGS_TABLE + " " +
|
||||||
|
"(key TEXT, value INT, PRIMARY KEY(key))");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanup() {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
// Clear outdated policies
|
||||||
|
db.delete(POLICY_TABLE, "until > 0 AND until < ?",
|
||||||
|
new String[] { String.valueOf(System.currentTimeMillis() / 1000) });
|
||||||
|
// Clear outdated logs
|
||||||
|
db.delete(LOG_TABLE, "time < ?", new String[] { String.valueOf(
|
||||||
|
System.currentTimeMillis() / 1000 - magiskManager.suLogTimeout * 86400) });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePolicy(Policy policy) {
|
||||||
|
deletePolicy(policy.packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePolicy(String pkg) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(POLICY_TABLE, "package_name=?", new String[] { pkg });
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePolicy(int uid) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
deletePolicy(db, uid);
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deletePolicy(SQLiteDatabase db, int uid) {
|
||||||
|
db.delete(POLICY_TABLE, "uid=?", new String[]{String.valueOf(uid)});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Policy getPolicy(int uid) {
|
||||||
|
Policy policy = null;
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
try (Cursor c = db.query(POLICY_TABLE, null, "uid=?", new String[] { String.valueOf(uid) }, null, null, null)) {
|
||||||
|
if (c.moveToNext()) {
|
||||||
|
policy = new Policy(c, pm);
|
||||||
|
}
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
deletePolicy(uid);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Policy getPolicy(String pkg) {
|
||||||
|
Policy policy = null;
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
try (Cursor c = db.query(POLICY_TABLE, null, "package_name=?", new String[] { pkg }, null, null, null)) {
|
||||||
|
if (c.moveToNext()) {
|
||||||
|
policy = new Policy(c, pm);
|
||||||
|
}
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
deletePolicy(pkg);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPolicy(Policy policy) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.replace(POLICY_TABLE, null, policy.getContentValues());
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updatePolicy(Policy policy) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
updatePolicy(db, policy);
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updatePolicy(SQLiteDatabase db, Policy policy) {
|
||||||
|
db.update(POLICY_TABLE, policy.getContentValues(), "package_name=?",
|
||||||
|
new String[] { policy.packageName });
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Policy> getPolicyList(PackageManager pm) {
|
||||||
|
List<Policy> ret = new ArrayList<>();
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
Policy policy;
|
||||||
|
try (Cursor c = db.query(POLICY_TABLE, null, null, null, null, null, null)) {
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
try {
|
||||||
|
policy = new Policy(c, pm);
|
||||||
|
// The application changed UID for some reason, check user config
|
||||||
|
if (policy.info.uid != policy.uid) {
|
||||||
|
if (magiskManager.suReauth) {
|
||||||
|
// Reauth required, remove from DB
|
||||||
|
deletePolicy(db, policy.uid);
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
// No reauth, update to use the new UID
|
||||||
|
policy.uid = policy.info.uid;
|
||||||
|
updatePolicy(db, policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.add(policy);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
// The app no longer exist, remove from DB
|
||||||
|
deletePolicy(db, c.getInt(c.getColumnIndex("uid")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
Collections.sort(ret);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SuLogEntry> getLogList(SQLiteDatabase db, String selection) {
|
||||||
|
List<SuLogEntry> ret = new ArrayList<>();
|
||||||
|
try (Cursor c = db.query(LOG_TABLE, null, selection, null, null, null, "time DESC")) {
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
ret.add(new SuLogEntry(c));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void migrateLegacyLogList(File oldDB, SQLiteDatabase newDB) {
|
||||||
|
SQLiteDatabase db = SQLiteDatabase.openDatabase(oldDB.getPath(), null, SQLiteDatabase.OPEN_READWRITE);
|
||||||
|
List<SuLogEntry> logs = getLogList(db, null);
|
||||||
|
for (SuLogEntry log : logs) {
|
||||||
|
newDB.insert(LOG_TABLE, null, log.getContentValues());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SuLogEntry> getLogList() {
|
||||||
|
return getLogList(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<SuLogEntry> getLogList(String selection) {
|
||||||
|
return getLogList(getReadableDatabase(), selection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addLog(SuLogEntry log) {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.insert(LOG_TABLE, null, log.getContentValues());
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearLogs() {
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.delete(LOG_TABLE, null, null);
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSettings(String key, int value) {
|
||||||
|
ContentValues data = new ContentValues();
|
||||||
|
data.put("key", key);
|
||||||
|
data.put("value", value);
|
||||||
|
SQLiteDatabase db = getWritableDatabase();
|
||||||
|
db.replace(SETTINGS_TABLE, null, data);
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSettings(String key, int defaultValue) {
|
||||||
|
SQLiteDatabase db = getReadableDatabase();
|
||||||
|
int value = defaultValue;
|
||||||
|
try (Cursor c = db.query(SETTINGS_TABLE, null, "key=?", new String[] { key }, null, null, null)) {
|
||||||
|
while (c.moveToNext()) {
|
||||||
|
value = c.getInt(c.getColumnIndex("value"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db.close();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.module;
|
package com.topjohnwu.magisk.module;
|
||||||
|
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
@@ -9,66 +10,76 @@ import java.util.List;
|
|||||||
|
|
||||||
public abstract class BaseModule implements Comparable<BaseModule> {
|
public abstract class BaseModule implements Comparable<BaseModule> {
|
||||||
|
|
||||||
protected String mId, mName, mVersion, mAuthor, mDescription, mSupportUrl, mDonateUrl;
|
private String mId, mName, mVersion, mAuthor, mDescription;
|
||||||
protected boolean mIsCacheModule = false;
|
private int mVersionCode = 0, templateVersion = 0;
|
||||||
protected int mVersionCode = 0;
|
|
||||||
|
protected BaseModule() {}
|
||||||
|
|
||||||
|
protected BaseModule(Cursor c) {
|
||||||
|
mId = c.getString(c.getColumnIndex("id"));
|
||||||
|
mName = c.getString(c.getColumnIndex("name"));
|
||||||
|
mVersion = c.getString(c.getColumnIndex("version"));
|
||||||
|
mVersionCode = c.getInt(c.getColumnIndex("versionCode"));
|
||||||
|
mAuthor = c.getString(c.getColumnIndex("author"));
|
||||||
|
mDescription = c.getString(c.getColumnIndex("description"));
|
||||||
|
templateVersion = c.getInt(c.getColumnIndex("template"));
|
||||||
|
}
|
||||||
|
|
||||||
protected void parseProps(List<String> props) throws CacheModException { parseProps(props.toArray(new String[props.size()])); }
|
protected void parseProps(List<String> props) throws CacheModException { parseProps(props.toArray(new String[props.size()])); }
|
||||||
|
|
||||||
protected void parseProps(String[] props) throws CacheModException {
|
protected void parseProps(String[] props) throws CacheModException {
|
||||||
for (String line : props) {
|
for (String line : props) {
|
||||||
String[] prop = line.split("=", 2);
|
String[] prop = line.split("=", 2);
|
||||||
if (prop.length != 2) {
|
if (prop.length != 2)
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
String key = prop[0].trim();
|
String key = prop[0].trim();
|
||||||
if (key.charAt(0) == '#') {
|
if (key.charAt(0) == '#')
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case "id":
|
case "id":
|
||||||
this.mId = prop[1];
|
mId = prop[1];
|
||||||
break;
|
break;
|
||||||
case "name":
|
case "name":
|
||||||
this.mName = prop[1];
|
mName = prop[1];
|
||||||
break;
|
break;
|
||||||
case "version":
|
case "version":
|
||||||
this.mVersion = prop[1];
|
mVersion = prop[1];
|
||||||
break;
|
break;
|
||||||
case "versionCode":
|
case "versionCode":
|
||||||
try {
|
try {
|
||||||
this.mVersionCode = Integer.parseInt(prop[1]);
|
mVersionCode = Integer.parseInt(prop[1]);
|
||||||
} catch (NumberFormatException ignored) {}
|
} catch (NumberFormatException ignored) {}
|
||||||
break;
|
break;
|
||||||
case "author":
|
case "author":
|
||||||
this.mAuthor = prop[1];
|
mAuthor = prop[1];
|
||||||
break;
|
break;
|
||||||
case "description":
|
case "description":
|
||||||
this.mDescription = prop[1];
|
mDescription = prop[1];
|
||||||
break;
|
|
||||||
case "support":
|
|
||||||
this.mSupportUrl = prop[1];
|
|
||||||
break;
|
|
||||||
case "donate":
|
|
||||||
this.mDonateUrl = prop[1];
|
|
||||||
break;
|
break;
|
||||||
|
case "template":
|
||||||
|
try {
|
||||||
|
templateVersion = Integer.parseInt(prop[1]);
|
||||||
|
} catch (NumberFormatException ignored) {}
|
||||||
case "cacheModule":
|
case "cacheModule":
|
||||||
this.mIsCacheModule = Boolean.parseBoolean(prop[1]);
|
if (Boolean.parseBoolean(prop[1]))
|
||||||
|
throw new CacheModException(mId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mIsCacheModule)
|
|
||||||
throw new CacheModException(mId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return mName;
|
return mName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
mName = name;
|
||||||
|
}
|
||||||
|
|
||||||
public String getVersion() {
|
public String getVersion() {
|
||||||
return mVersion;
|
return mVersion;
|
||||||
}
|
}
|
||||||
@@ -77,7 +88,13 @@ public abstract class BaseModule implements Comparable<BaseModule> {
|
|||||||
return mAuthor;
|
return mAuthor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {return mId; }
|
public String getId() {
|
||||||
|
return mId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
mId = id;
|
||||||
|
}
|
||||||
|
|
||||||
public String getDescription() {
|
public String getDescription() {
|
||||||
return mDescription;
|
return mDescription;
|
||||||
@@ -87,22 +104,18 @@ public abstract class BaseModule implements Comparable<BaseModule> {
|
|||||||
return mVersionCode;
|
return mVersionCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDonateUrl() {
|
public int getTemplateVersion() {
|
||||||
return mDonateUrl;
|
return templateVersion;
|
||||||
}
|
|
||||||
|
|
||||||
public String getSupportUrl() {
|
|
||||||
return mSupportUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class CacheModException extends Exception {
|
public static class CacheModException extends Exception {
|
||||||
public CacheModException(String id) {
|
public CacheModException(String id) {
|
||||||
Logger.dev("Cache mods are no longer supported! id: " + id);
|
Logger.error("Cache mods are no longer supported! id: " + id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(@NonNull BaseModule o) {
|
public int compareTo(@NonNull BaseModule module) {
|
||||||
return this.getName().toLowerCase().compareTo(o.getName().toLowerCase());
|
return this.getName().toLowerCase().compareTo(module.getName().toLowerCase());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,15 +16,16 @@ public class Module extends BaseModule {
|
|||||||
mDisableFile = path + "/disable";
|
mDisableFile = path + "/disable";
|
||||||
mUpdateFile = path + "/update";
|
mUpdateFile = path + "/update";
|
||||||
|
|
||||||
if (mId == null) {
|
if (getId() == null) {
|
||||||
int sep = path.lastIndexOf('/');
|
int sep = path.lastIndexOf('/');
|
||||||
mId = path.substring(sep + 1);
|
setId(path.substring(sep + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mName == null)
|
if (getName() == null) {
|
||||||
mName = mId;
|
setName(getId());
|
||||||
|
}
|
||||||
|
|
||||||
Logger.dev("Creating Module, id: " + mId);
|
Logger.dev("Creating Module, id: " + getId());
|
||||||
|
|
||||||
mEnable = !Utils.itemExist(mDisableFile);
|
mEnable = !Utils.itemExist(mDisableFile);
|
||||||
mRemove = Utils.itemExist(mRemoveFile);
|
mRemove = Utils.itemExist(mRemoveFile);
|
||||||
@@ -32,11 +33,13 @@ public class Module extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void createDisableFile() {
|
public void createDisableFile() {
|
||||||
mEnable = !Utils.createFile(mDisableFile);
|
mEnable = false;
|
||||||
|
Utils.createFile(mDisableFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeDisableFile() {
|
public void removeDisableFile() {
|
||||||
mEnable = Utils.removeItem(mDisableFile);
|
mEnable = true;
|
||||||
|
Utils.removeItem(mDisableFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
@@ -44,11 +47,13 @@ public class Module extends BaseModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void createRemoveFile() {
|
public void createRemoveFile() {
|
||||||
mRemove = Utils.createFile(mRemoveFile);
|
mRemove = true;
|
||||||
|
Utils.createFile(mRemoveFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteRemoveFile() {
|
public void deleteRemoveFile() {
|
||||||
mRemove = !Utils.removeItem(mRemoveFile);
|
mRemove = false;
|
||||||
|
Utils.removeItem(mRemoveFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean willBeRemoved() {
|
public boolean willBeRemoved() {
|
||||||
|
|||||||
@@ -1,53 +1,71 @@
|
|||||||
package com.topjohnwu.magisk.module;
|
package com.topjohnwu.magisk.module;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Logger;
|
import com.topjohnwu.magisk.utils.Logger;
|
||||||
import com.topjohnwu.magisk.utils.WebRequest;
|
import com.topjohnwu.magisk.utils.WebService;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
public class Repo extends BaseModule {
|
public class Repo extends BaseModule {
|
||||||
private String mLogUrl, mManifestUrl, mZipUrl;
|
|
||||||
|
private static final String FILE_URL = "https://raw.githubusercontent.com/Magisk-Modules-Repo/%s/master/%s";
|
||||||
|
private static final String ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip";
|
||||||
|
|
||||||
|
private String repoName;
|
||||||
private Date mLastUpdate;
|
private Date mLastUpdate;
|
||||||
|
|
||||||
public Repo(Context context, String name, Date lastUpdate) throws CacheModException {
|
public Repo(String name, Date lastUpdate) throws CacheModException {
|
||||||
mLastUpdate = lastUpdate;
|
mLastUpdate = lastUpdate;
|
||||||
mLogUrl = context.getString(R.string.file_url, name, "changelog.txt");
|
repoName = name;
|
||||||
mManifestUrl = context.getString(R.string.file_url, name, "module.prop");
|
|
||||||
mZipUrl = context.getString(R.string.zip_url, name);
|
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Repo(Cursor c) {
|
||||||
|
super(c);
|
||||||
|
repoName = c.getString(c.getColumnIndex("repo_name"));
|
||||||
|
mLastUpdate = new Date(c.getLong(c.getColumnIndex("last_update")));
|
||||||
|
}
|
||||||
|
|
||||||
public void update() throws CacheModException {
|
public void update() throws CacheModException {
|
||||||
Logger.dev("Repo: Re-fetch prop");
|
String props = WebService.request(getManifestUrl(), WebService.GET);
|
||||||
String props = WebRequest.makeWebServiceCall(mManifestUrl, WebRequest.GET, true);
|
|
||||||
String lines[] = props.split("\\n");
|
String lines[] = props.split("\\n");
|
||||||
parseProps(lines);
|
parseProps(lines);
|
||||||
|
Logger.dev("Repo: Fetching prop: " + getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(Date lastUpdate) throws CacheModException {
|
public void update(Date lastUpdate) throws CacheModException {
|
||||||
Logger.dev("Repo: Old: " + mLastUpdate);
|
|
||||||
Logger.dev("Repo: New: " + lastUpdate);
|
|
||||||
if (mIsCacheModule)
|
|
||||||
throw new CacheModException(mId);
|
|
||||||
if (lastUpdate.after(mLastUpdate)) {
|
if (lastUpdate.after(mLastUpdate)) {
|
||||||
mLastUpdate = lastUpdate;
|
mLastUpdate = lastUpdate;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getZipUrl() {
|
public ContentValues getContentValues() {
|
||||||
return mZipUrl;
|
ContentValues values = new ContentValues();
|
||||||
|
values.put("id", getId());
|
||||||
|
values.put("name", getName());
|
||||||
|
values.put("version", getVersion());
|
||||||
|
values.put("versionCode", getVersionCode());
|
||||||
|
values.put("author", getAuthor());
|
||||||
|
values.put("description", getDescription());
|
||||||
|
values.put("repo_name", repoName);
|
||||||
|
values.put("last_update", mLastUpdate.getTime());
|
||||||
|
values.put("template", getTemplateVersion());
|
||||||
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLogUrl() {
|
public String getZipUrl() {
|
||||||
return mLogUrl;
|
return String.format(ZIP_URL, repoName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getManifestUrl() {
|
public String getManifestUrl() {
|
||||||
return mManifestUrl;
|
return String.format(FILE_URL, repoName, "module.prop");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDetailUrl() {
|
||||||
|
return String.format(FILE_URL, repoName, "README.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getLastUpdate() {
|
public Date getLastUpdate() {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.topjohnwu.magisk.receivers;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.services.OnBootIntentService;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
public class BootReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private void startIntentService(Context context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(new Intent(context, OnBootIntentService.class));
|
||||||
|
} else {
|
||||||
|
context.startService(new Intent(context, OnBootIntentService.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
|
||||||
|
Utils.getMagiskManager(context).initSU();
|
||||||
|
// There is currently no need to start an IntentService onBoot
|
||||||
|
// startIntentService(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.receivers;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.StatusFragment;
|
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
|
||||||
import com.topjohnwu.magisk.utils.Shell;
|
|
||||||
import com.topjohnwu.magisk.utils.ZipUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
public class MagiskDlReceiver extends DownloadReceiver {
|
|
||||||
|
|
||||||
String mBoot;
|
|
||||||
boolean mEnc, mVerity;
|
|
||||||
|
|
||||||
public MagiskDlReceiver(String bootImage, boolean keepEnc, boolean keepVerity) {
|
|
||||||
mBoot = bootImage;
|
|
||||||
mEnc = keepEnc;
|
|
||||||
mVerity = keepVerity;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDownloadDone(Uri uri) {
|
|
||||||
new Async.FlashZIP(mContext, uri, mFilename) {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void preProcessing() throws Throwable {
|
|
||||||
Shell.su(
|
|
||||||
"echo \"BOOTIMAGE=/dev/block/" + mBoot + "\" > /dev/.magisk",
|
|
||||||
"echo \"KEEPFORCEENCRYPT=" + String.valueOf(mEnc) + "\" >> /dev/.magisk",
|
|
||||||
"echo \"KEEPVERITY=" + String.valueOf(mVerity) + "\" >> /dev/.magisk"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean unzipAndCheck() {
|
|
||||||
publishProgress(mContext.getString(R.string.zip_install_unzip_zip_msg));
|
|
||||||
if (Shell.rootAccess()) {
|
|
||||||
// We might not have busybox yet, unzip with Java
|
|
||||||
// We will have complete busybox after Magisk installation
|
|
||||||
ZipUtils.unzip(mCachedFile, new File(mCachedFile.getParent(), "magisk"));
|
|
||||||
Shell.su(
|
|
||||||
"mkdir -p " + Async.TMP_FOLDER_PATH + "/magisk",
|
|
||||||
"cp -af " + mCachedFile.getParent() + "/magisk/. " + Async.TMP_FOLDER_PATH + "/magisk",
|
|
||||||
"mv -f " + mCachedFile.getParent() + "/magisk/META-INF " + mCachedFile.getParent() + "/META-INF"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSuccess() {
|
|
||||||
new Async.RootTask<Void, Void, Void>() {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
Shell.su("setprop magisk.version "
|
|
||||||
+ String.valueOf(StatusFragment.remoteMagiskVersion));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
super.onSuccess();
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.topjohnwu.magisk.receivers;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.v4.content.FileProvider;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class ManagerUpdate extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
Utils.dlAndReceive(
|
||||||
|
context,
|
||||||
|
new DownloadReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onDownloadDone(Uri uri) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
Intent install = new Intent(Intent.ACTION_INSTALL_PACKAGE);
|
||||||
|
install.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
Uri content = FileProvider.getUriForFile(mContext,
|
||||||
|
"com.topjohnwu.magisk.provider", new File(uri.getPath()));
|
||||||
|
install.setData(content);
|
||||||
|
mContext.startActivity(install);
|
||||||
|
} else {
|
||||||
|
Intent install = new Intent(Intent.ACTION_VIEW);
|
||||||
|
install.setDataAndType(uri, "application/vnd.android.package-archive");
|
||||||
|
install.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
mContext.startActivity(install);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
intent.getStringExtra("link"),
|
||||||
|
Utils.getLegalFilename("MagiskManager-v" +
|
||||||
|
intent.getStringExtra("version") + ".apk"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.topjohnwu.magisk.receivers;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.superuser.Policy;
|
||||||
|
import com.topjohnwu.magisk.utils.Utils;
|
||||||
|
|
||||||
|
public class PackageReceiver extends BroadcastReceiver {
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
MagiskManager magiskManager = Utils.getMagiskManager(context);
|
||||||
|
magiskManager.initSUConfig();
|
||||||
|
|
||||||
|
String pkg = intent.getData().getEncodedSchemeSpecificPart();
|
||||||
|
Policy policy = magiskManager.suDB.getPolicy(pkg);
|
||||||
|
if (policy == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (intent.getAction()) {
|
||||||
|
case Intent.ACTION_PACKAGE_REPLACED:
|
||||||
|
// This will only work pre-O
|
||||||
|
if (magiskManager.suReauth) {
|
||||||
|
magiskManager.suDB.deletePolicy(policy);
|
||||||
|
} else {
|
||||||
|
int uid = intent.getIntExtra(Intent.EXTRA_UID, -1);
|
||||||
|
// Update the UID if available
|
||||||
|
if (uid > 0) {
|
||||||
|
policy.uid = uid % 100000;
|
||||||
|
}
|
||||||
|
magiskManager.suDB.updatePolicy(policy);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Intent.ACTION_PACKAGE_FULLY_REMOVED:
|
||||||
|
magiskManager.suDB.deletePolicy(policy);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.receivers;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.utils.Async;
|
|
||||||
import com.topjohnwu.magisk.utils.Utils.ByteArrayInOutStream;
|
|
||||||
import com.topjohnwu.magisk.utils.ZipUtils;
|
|
||||||
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class RepoDlReceiver extends DownloadReceiver {
|
|
||||||
@Override
|
|
||||||
public void onDownloadDone(Uri uri) {
|
|
||||||
// Flash the zip
|
|
||||||
new Async.FlashZIP(mContext, uri, mFilename){
|
|
||||||
@Override
|
|
||||||
protected void preProcessing() throws Throwable {
|
|
||||||
// Process and sign the zip
|
|
||||||
publishProgress(mContext.getString(R.string.zip_install_process_zip_msg));
|
|
||||||
ByteArrayInOutStream buffer = new ByteArrayInOutStream();
|
|
||||||
|
|
||||||
// First remove top folder (the folder with the repo name) in Github source zip
|
|
||||||
ZipUtils.removeTopFolder(mContext.getContentResolver().openInputStream(mUri), buffer);
|
|
||||||
|
|
||||||
// Then sign the zip for the first time
|
|
||||||
ZipUtils.signZip(mContext, buffer.getInputStream(), buffer, false);
|
|
||||||
|
|
||||||
// Adjust the zip to prevent unzip issues
|
|
||||||
ZipUtils.adjustZip(buffer);
|
|
||||||
|
|
||||||
// Finally, sign the whole zip file again
|
|
||||||
ZipUtils.signZip(mContext, buffer.getInputStream(), buffer, true);
|
|
||||||
|
|
||||||
// Write it back to the downloaded zip
|
|
||||||
OutputStream out = mContext.getContentResolver().openOutputStream(mUri);
|
|
||||||
buffer.writeTo(out);
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
}.exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.topjohnwu.magisk.services;
|
||||||
|
|
||||||
|
import android.app.IntentService;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.support.v7.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
|
public class OnBootIntentService extends IntentService {
|
||||||
|
|
||||||
|
private static final int ONBOOT_NOTIFICATION_ID = 3;
|
||||||
|
|
||||||
|
public OnBootIntentService() {
|
||||||
|
super("OnBootIntentService");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
|
||||||
|
builder.setSmallIcon(R.drawable.ic_magisk)
|
||||||
|
.setContentTitle("onBoot")
|
||||||
|
.setContentText("Running onBoot operations...");
|
||||||
|
startForeground(ONBOOT_NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHandleIntent(Intent intent) {
|
||||||
|
// Currently nothing to do
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.topjohnwu.magisk.services;
|
||||||
|
|
||||||
|
import android.app.job.JobParameters;
|
||||||
|
import android.app.job.JobService;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.asyncs.CheckUpdates;
|
||||||
|
|
||||||
|
public class UpdateCheckService extends JobService {
|
||||||
|
@Override
|
||||||
|
public boolean onStartJob(JobParameters params) {
|
||||||
|
new CheckUpdates(this, true){
|
||||||
|
@Override
|
||||||
|
protected Void doInBackground(Void... voids) {
|
||||||
|
magiskManager.updateMagiskInfo();
|
||||||
|
magiskManager.updateNotification = magiskManager.prefs.getBoolean("notification", true);
|
||||||
|
return super.doInBackground(voids);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Void v) {
|
||||||
|
jobFinished(params, false);
|
||||||
|
super.onPostExecute(v);
|
||||||
|
}
|
||||||
|
}.exec();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onStopJob(JobParameters params) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/src/main/java/com/topjohnwu/magisk/superuser/Policy.java
Normal file
57
app/src/main/java/com/topjohnwu/magisk/superuser/Policy.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package com.topjohnwu.magisk.superuser;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.pm.ApplicationInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
|
||||||
|
public class Policy implements Comparable<Policy>{
|
||||||
|
public static final int INTERACTIVE = 0;
|
||||||
|
public static final int DENY = 1;
|
||||||
|
public static final int ALLOW = 2;
|
||||||
|
|
||||||
|
public int uid, policy = INTERACTIVE;
|
||||||
|
public long until;
|
||||||
|
public boolean logging = true, notification = true;
|
||||||
|
public String packageName, appName;
|
||||||
|
public ApplicationInfo info;
|
||||||
|
|
||||||
|
public Policy(int uid, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||||
|
String[] pkgs = pm.getPackagesForUid(uid);
|
||||||
|
if (pkgs != null && pkgs.length > 0) {
|
||||||
|
this.uid = uid;
|
||||||
|
packageName = pkgs[0];
|
||||||
|
info = pm.getApplicationInfo(packageName, 0);
|
||||||
|
appName = info.loadLabel(pm).toString();
|
||||||
|
} else throw new PackageManager.NameNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Policy(Cursor c, PackageManager pm) throws PackageManager.NameNotFoundException {
|
||||||
|
uid = c.getInt(c.getColumnIndex("uid"));
|
||||||
|
packageName = c.getString(c.getColumnIndex("package_name"));
|
||||||
|
policy = c.getInt(c.getColumnIndex("policy"));
|
||||||
|
until = c.getLong(c.getColumnIndex("until"));
|
||||||
|
logging = c.getInt(c.getColumnIndex("logging")) != 0;
|
||||||
|
notification = c.getInt(c.getColumnIndex("notification")) != 0;
|
||||||
|
info = pm.getApplicationInfo(packageName, 0);
|
||||||
|
appName = info.loadLabel(pm).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ContentValues getContentValues() {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
|
values.put("uid", uid);
|
||||||
|
values.put("package_name", packageName);
|
||||||
|
values.put("policy", policy);
|
||||||
|
values.put("until", until);
|
||||||
|
values.put("logging", logging ? 1 : 0);
|
||||||
|
values.put("notification", notification ? 1 : 0);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NonNull Policy policy) {
|
||||||
|
return appName.toLowerCase().compareTo(policy.appName.toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.topjohnwu.magisk.superuser;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.components.Activity;
|
||||||
|
|
||||||
|
public class RequestActivity extends Activity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
Intent intent = getIntent();
|
||||||
|
if (intent == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
getApplicationContext().initSUConfig();
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).setClass(this, SuRequestActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
package com.topjohnwu.magisk.superuser;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class SuLogEntry implements Parcelable {
|
||||||
|
|
||||||
|
public int fromUid, toUid, fromPid;
|
||||||
|
public String packageName, appName, command;
|
||||||
|
public boolean action;
|
||||||
|
public Date date;
|
||||||
|
|
||||||
|
public SuLogEntry(Policy policy) {
|
||||||
|
fromUid = policy.uid;
|
||||||
|
packageName = policy.packageName;
|
||||||
|
appName = policy.appName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SuLogEntry(Cursor c) {
|
||||||
|
fromUid = c.getInt(c.getColumnIndex("from_uid"));
|
||||||
|
fromPid = c.getInt(c.getColumnIndex("from_pid"));
|
||||||
|
toUid = c.getInt(c.getColumnIndex("to_uid"));
|
||||||
|
packageName = c.getString(c.getColumnIndex("package_name"));
|
||||||
|
appName = c.getString(c.getColumnIndex("app_name"));
|
||||||
|
command = c.getString(c.getColumnIndex("command"));
|
||||||
|
action = c.getInt(c.getColumnIndex("action")) != 0;
|
||||||
|
date = new Date(c.getLong(c.getColumnIndex("time")) * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() / 1000);
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDateString() {
|
||||||
|
return DateFormat.getDateInstance(DateFormat.MEDIUM).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTimeString() {
|
||||||
|
return new SimpleDateFormat("h:mm a", Locale.US).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static final Creator<SuLogEntry> CREATOR = new Creator<SuLogEntry>() {
|
||||||
|
@Override
|
||||||
|
public SuLogEntry createFromParcel(Parcel in) {
|
||||||
|
return new SuLogEntry(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SuLogEntry[] newArray(int size) {
|
||||||
|
return new SuLogEntry[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
protected SuLogEntry(Parcel in) {
|
||||||
|
fromUid = in.readInt();
|
||||||
|
toUid = in.readInt();
|
||||||
|
fromPid = in.readInt();
|
||||||
|
packageName = in.readString();
|
||||||
|
appName = in.readString();
|
||||||
|
command = in.readString();
|
||||||
|
action = in.readByte() != 0;
|
||||||
|
date = new Date(in.readLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeInt(fromUid);
|
||||||
|
dest.writeInt(toUid);
|
||||||
|
dest.writeInt(fromPid);
|
||||||
|
dest.writeString(packageName);
|
||||||
|
dest.writeString(appName);
|
||||||
|
dest.writeString(command);
|
||||||
|
dest.writeByte((byte) (action ? 1 : 0));
|
||||||
|
dest.writeLong(date.getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
package com.topjohnwu.magisk.superuser;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Process;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class SuReceiver extends BroadcastReceiver {
|
||||||
|
|
||||||
|
private static final int NO_NOTIFICATION = 0;
|
||||||
|
private static final int TOAST = 1;
|
||||||
|
private static final int NOTIFY_NORMAL_LOG = 0;
|
||||||
|
private static final int NOTIFY_USER_TOASTS = 1;
|
||||||
|
private static final int NOTIFY_USER_TO_OWNER = 2;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReceive(Context context, Intent intent) {
|
||||||
|
int fromUid, toUid, pid, mode;
|
||||||
|
String command, action;
|
||||||
|
Policy policy;
|
||||||
|
|
||||||
|
MagiskManager magiskManager = (MagiskManager) context.getApplicationContext();
|
||||||
|
magiskManager.initSUConfig();
|
||||||
|
|
||||||
|
if (intent == null) return;
|
||||||
|
|
||||||
|
mode = intent.getIntExtra("mode", -1);
|
||||||
|
if (mode < 0) return;
|
||||||
|
|
||||||
|
if (mode == NOTIFY_USER_TO_OWNER) {
|
||||||
|
magiskManager.toast(R.string.multiuser_hint_owner_request, Toast.LENGTH_LONG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromUid = intent.getIntExtra("from.uid", -1);
|
||||||
|
if (fromUid < 0) return;
|
||||||
|
if (fromUid == Process.myUid()) return; // Don't show anything if it's Magisk Manager
|
||||||
|
|
||||||
|
action = intent.getStringExtra("action");
|
||||||
|
if (action == null) return;
|
||||||
|
|
||||||
|
policy = magiskManager.suDB.getPolicy(fromUid);
|
||||||
|
if (policy == null) {
|
||||||
|
try {
|
||||||
|
policy = new Policy(fromUid, context.getPackageManager());
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SuLogEntry log = new SuLogEntry(policy);
|
||||||
|
|
||||||
|
String message;
|
||||||
|
switch (action) {
|
||||||
|
case "allow":
|
||||||
|
message = context.getString(R.string.su_allow_toast, policy.appName);
|
||||||
|
log.action = true;
|
||||||
|
break;
|
||||||
|
case "deny":
|
||||||
|
message = context.getString(R.string.su_deny_toast, policy.appName);
|
||||||
|
log.action = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (policy.notification && magiskManager.suNotificationType == TOAST) {
|
||||||
|
magiskManager.toast(message, Toast.LENGTH_SHORT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == NOTIFY_NORMAL_LOG && policy.logging) {
|
||||||
|
toUid = intent.getIntExtra("to.uid", -1);
|
||||||
|
if (toUid < 0) return;
|
||||||
|
pid = intent.getIntExtra("pid", -1);
|
||||||
|
if (pid < 0) return;
|
||||||
|
command = intent.getStringExtra("command");
|
||||||
|
if (command == null) return;
|
||||||
|
log.toUid = toUid;
|
||||||
|
log.fromPid = pid;
|
||||||
|
log.command = command;
|
||||||
|
log.date = new Date();
|
||||||
|
magiskManager.suDB.addLog(log);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package com.topjohnwu.magisk.superuser;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.LocalSocket;
|
||||||
|
import android.net.LocalSocketAddress;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.CountDownTimer;
|
||||||
|
import android.os.FileObserver;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.asyncs.ParallelTask;
|
||||||
|
import com.topjohnwu.magisk.components.Activity;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
|
||||||
|
public class SuRequestActivity extends Activity {
|
||||||
|
|
||||||
|
private static final int[] timeoutList = {0, -1, 10, 20, 30, 60};
|
||||||
|
private static final int PROMPT = 0;
|
||||||
|
private static final int AUTO_DENY = 1;
|
||||||
|
private static final int AUTO_ALLOW = 2;
|
||||||
|
|
||||||
|
@BindView(R.id.su_popup) LinearLayout suPopup;
|
||||||
|
@BindView(R.id.timeout) Spinner timeout;
|
||||||
|
@BindView(R.id.app_icon) ImageView appIcon;
|
||||||
|
@BindView(R.id.app_name) TextView appNameView;
|
||||||
|
@BindView(R.id.package_name) TextView packageNameView;
|
||||||
|
@BindView(R.id.grant_btn) Button grant_btn;
|
||||||
|
@BindView(R.id.deny_btn) Button deny_btn;
|
||||||
|
|
||||||
|
private String socketPath;
|
||||||
|
private LocalSocket socket;
|
||||||
|
private PackageManager pm;
|
||||||
|
private MagiskManager magiskManager;
|
||||||
|
|
||||||
|
private boolean hasTimeout;
|
||||||
|
private Policy policy;
|
||||||
|
private CountDownTimer timer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
|
pm = getPackageManager();
|
||||||
|
magiskManager = getApplicationContext();
|
||||||
|
|
||||||
|
Intent intent = getIntent();
|
||||||
|
socketPath = intent.getStringExtra("socket");
|
||||||
|
hasTimeout = intent.getBooleanExtra("timeout", true);
|
||||||
|
|
||||||
|
new FileObserver(socketPath) {
|
||||||
|
@Override
|
||||||
|
public void onEvent(int fileEvent, String path) {
|
||||||
|
if (fileEvent == FileObserver.DELETE_SELF) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.startWatching();
|
||||||
|
|
||||||
|
new SocketManager(this).exec();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean cancelTimeout() {
|
||||||
|
timer.cancel();
|
||||||
|
deny_btn.setText(getString(R.string.deny));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showRequest() {
|
||||||
|
switch (magiskManager.suResponseType) {
|
||||||
|
case AUTO_DENY:
|
||||||
|
handleAction(Policy.DENY, 0);
|
||||||
|
return;
|
||||||
|
case AUTO_ALLOW:
|
||||||
|
handleAction(Policy.ALLOW, 0);
|
||||||
|
return;
|
||||||
|
case PROMPT:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not interactive, response directly
|
||||||
|
if (policy.policy != Policy.INTERACTIVE) {
|
||||||
|
handleAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_request);
|
||||||
|
ButterKnife.bind(this);
|
||||||
|
|
||||||
|
appIcon.setImageDrawable(policy.info.loadIcon(pm));
|
||||||
|
appNameView.setText(policy.appName);
|
||||||
|
packageNameView.setText(policy.packageName);
|
||||||
|
|
||||||
|
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this,
|
||||||
|
R.array.allow_timeout, android.R.layout.simple_spinner_item);
|
||||||
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
|
timeout.setAdapter(adapter);
|
||||||
|
|
||||||
|
timer = new CountDownTimer(magiskManager.suRequestTimeout * 1000, 1000) {
|
||||||
|
@Override
|
||||||
|
public void onTick(long millisUntilFinished) {
|
||||||
|
deny_btn.setText(getString(R.string.deny_with_str, "(" + millisUntilFinished / 1000 + ")"));
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void onFinish() {
|
||||||
|
deny_btn.setText(getString(R.string.deny_with_str, "(0)"));
|
||||||
|
handleAction(Policy.DENY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
grant_btn.setOnClickListener(v -> {
|
||||||
|
handleAction(Policy.ALLOW);
|
||||||
|
timer.cancel();
|
||||||
|
});
|
||||||
|
deny_btn.setOnClickListener(v -> {
|
||||||
|
handleAction(Policy.DENY);
|
||||||
|
timer.cancel();
|
||||||
|
});
|
||||||
|
suPopup.setOnClickListener(v -> cancelTimeout());
|
||||||
|
timeout.setOnTouchListener((v, event) -> cancelTimeout());
|
||||||
|
|
||||||
|
if (hasTimeout) {
|
||||||
|
timer.start();
|
||||||
|
} else {
|
||||||
|
cancelTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (policy != null) {
|
||||||
|
handleAction(Policy.DENY);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAction() {
|
||||||
|
String response;
|
||||||
|
if (policy.policy == Policy.ALLOW) {
|
||||||
|
response = "socket:ALLOW";
|
||||||
|
} else {
|
||||||
|
response = "socket:DENY";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
socket.getOutputStream().write((response).getBytes());
|
||||||
|
socket.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAction(int action) {
|
||||||
|
handleAction(action, timeoutList[timeout.getSelectedItemPosition()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleAction(int action, int time) {
|
||||||
|
policy.policy = action;
|
||||||
|
if (time >= 0) {
|
||||||
|
policy.until = (time == 0) ? 0 : (System.currentTimeMillis() / 1000 + time * 60);
|
||||||
|
magiskManager.suDB.addPolicy(policy);
|
||||||
|
}
|
||||||
|
handleAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SocketManager extends ParallelTask<Void, Void, Boolean> {
|
||||||
|
|
||||||
|
SocketManager(Activity context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
try {
|
||||||
|
socket = new LocalSocket();
|
||||||
|
socket.connect(new LocalSocketAddress(socketPath, LocalSocketAddress.Namespace.FILESYSTEM));
|
||||||
|
|
||||||
|
DataInputStream is = new DataInputStream(socket.getInputStream());
|
||||||
|
ContentValues payload = new ContentValues();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
int nameLen = is.readInt();
|
||||||
|
byte[] nameBytes = new byte[nameLen];
|
||||||
|
is.readFully(nameBytes);
|
||||||
|
String name = new String(nameBytes);
|
||||||
|
if (TextUtils.equals(name, "eof"))
|
||||||
|
break;
|
||||||
|
|
||||||
|
int dataLen = is.readInt();
|
||||||
|
byte[] dataBytes = new byte[dataLen];
|
||||||
|
is.readFully(dataBytes);
|
||||||
|
String data = new String(dataBytes);
|
||||||
|
payload.put(name, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.getAsInteger("uid") == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int uid = payload.getAsInteger("uid");
|
||||||
|
policy = magiskManager.suDB.getPolicy(uid);
|
||||||
|
if (policy == null) {
|
||||||
|
policy = new Policy(uid, pm);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
if (result) {
|
||||||
|
showRequest();
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
|
||||||
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.InstallFragment;
|
|
||||||
import com.topjohnwu.magisk.MainActivity;
|
|
||||||
import com.topjohnwu.magisk.ModulesFragment;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.ReposFragment;
|
|
||||||
import com.topjohnwu.magisk.StatusFragment;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class Async {
|
|
||||||
|
|
||||||
public abstract static class RootTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
|
||||||
@SafeVarargs
|
|
||||||
public final void exec(Params... params) {
|
|
||||||
executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract static class NormalTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
|
|
||||||
@SafeVarargs
|
|
||||||
public final void exec(Params... params) {
|
|
||||||
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final String UPDATE_JSON = "https://raw.githubusercontent.com/topjohnwu/MagiskManager/updates/magisk_update.json";
|
|
||||||
public static final String MAGISK_HIDE_PATH = "/magisk/.core/magiskhide/";
|
|
||||||
public static final String TMP_FOLDER_PATH = "/dev/tmp";
|
|
||||||
|
|
||||||
public static class CheckUpdates extends NormalTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
String jsonStr = WebRequest.makeWebServiceCall(UPDATE_JSON, WebRequest.GET);
|
|
||||||
try {
|
|
||||||
JSONObject json = new JSONObject(jsonStr);
|
|
||||||
|
|
||||||
JSONObject magisk = json.getJSONObject("magisk");
|
|
||||||
|
|
||||||
StatusFragment.remoteMagiskVersion = magisk.getDouble("versionCode");
|
|
||||||
StatusFragment.magiskLink = magisk.getString("link");
|
|
||||||
StatusFragment.magiskChangelog = magisk.getString("changelog");
|
|
||||||
} catch (JSONException ignored) {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
StatusFragment.updateCheckDone.trigger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void checkSafetyNet(Context context) {
|
|
||||||
new SafetyNetHelper(context) {
|
|
||||||
@Override
|
|
||||||
public void handleResults(int i) {
|
|
||||||
StatusFragment.SNCheckResult = i;
|
|
||||||
StatusFragment.safetyNetDone.trigger();
|
|
||||||
}
|
|
||||||
}.requestTest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LoadModules extends RootTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
ModuleHelper.createModuleMap();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
ModulesFragment.moduleLoadDone.trigger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class LoadRepos extends NormalTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
private Context mContext;
|
|
||||||
|
|
||||||
public LoadRepos(Context context) {
|
|
||||||
mContext = context;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... voids) {
|
|
||||||
ModuleHelper.createRepoMap(mContext);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void v) {
|
|
||||||
ReposFragment.repoLoadDone.trigger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class FlashZIP extends RootTask<Void, String, Integer> {
|
|
||||||
|
|
||||||
protected Uri mUri;
|
|
||||||
protected File mCachedFile;
|
|
||||||
private String mFilename;
|
|
||||||
protected ProgressDialog progress;
|
|
||||||
private Context mContext;
|
|
||||||
|
|
||||||
public FlashZIP(Context context, Uri uri, String filename) {
|
|
||||||
mContext = context;
|
|
||||||
mUri = uri;
|
|
||||||
mFilename = filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
public FlashZIP(Context context, Uri uri) {
|
|
||||||
mContext = context;
|
|
||||||
mUri = uri;
|
|
||||||
|
|
||||||
// Try to get the filename ourselves
|
|
||||||
Cursor c = mContext.getContentResolver().query(uri, null, null, null, null);
|
|
||||||
if (c != null) {
|
|
||||||
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
|
||||||
c.moveToFirst();
|
|
||||||
if (nameIndex != -1) {
|
|
||||||
mFilename = c.getString(nameIndex);
|
|
||||||
}
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
if (mFilename == null) {
|
|
||||||
int idx = uri.getPath().lastIndexOf('/');
|
|
||||||
mFilename = uri.getPath().substring(idx + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void preProcessing() throws Throwable {}
|
|
||||||
|
|
||||||
protected void copyToCache() throws Throwable {
|
|
||||||
publishProgress(mContext.getString(R.string.copying_msg));
|
|
||||||
try {
|
|
||||||
InputStream in = mContext.getContentResolver().openInputStream(mUri);
|
|
||||||
mCachedFile = new File(mContext.getCacheDir().getAbsolutePath() + "/install.zip");
|
|
||||||
if (mCachedFile.exists() && !mCachedFile.delete()) {
|
|
||||||
throw new IOException();
|
|
||||||
}
|
|
||||||
OutputStream outputStream = new FileOutputStream(mCachedFile);
|
|
||||||
byte buffer[] = new byte[1024];
|
|
||||||
int length;
|
|
||||||
while ((length = in.read(buffer)) > 0) {
|
|
||||||
outputStream.write(buffer, 0, length);
|
|
||||||
}
|
|
||||||
outputStream.close();
|
|
||||||
Logger.dev("FlashZip: File created successfully - " + mCachedFile.getPath());
|
|
||||||
in.close();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.e(Logger.TAG, "FlashZip: Invalid Uri");
|
|
||||||
throw e;
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(Logger.TAG, "FlashZip: Error in creating file");
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected boolean unzipAndCheck() {
|
|
||||||
ZipUtils.unzip(mCachedFile, mCachedFile.getParentFile(), "META-INF/com/google/android");
|
|
||||||
return Utils.readFile(mCachedFile.getParent() + "/META-INF/com/google/android/updater-script")
|
|
||||||
.get(0).contains("#MAGISK");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
progress = new ProgressDialog(mContext);
|
|
||||||
progress.setTitle(R.string.zip_install_progress_title);
|
|
||||||
progress.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(String... values) {
|
|
||||||
progress.setMessage(values[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Integer doInBackground(Void... voids) {
|
|
||||||
Logger.dev("FlashZip Running... " + mFilename);
|
|
||||||
List<String> ret;
|
|
||||||
try {
|
|
||||||
preProcessing();
|
|
||||||
copyToCache();
|
|
||||||
} catch (Throwable e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (!unzipAndCheck()) return 0;
|
|
||||||
if (Shell.rootAccess()) {
|
|
||||||
publishProgress(mContext.getString(R.string.zip_install_progress_msg, mFilename));
|
|
||||||
ret = Shell.su(
|
|
||||||
"BOOTMODE=true sh " + mCachedFile.getParent() +
|
|
||||||
"/META-INF/com/google/android/update-binary dummy 1 " + mCachedFile.getPath(),
|
|
||||||
"if [ $? -eq 0 ]; then echo true; else echo false; fi"
|
|
||||||
);
|
|
||||||
Logger.dev("FlashZip: Console log:");
|
|
||||||
for (String line : ret) {
|
|
||||||
Logger.dev(line);
|
|
||||||
}
|
|
||||||
Shell.su(
|
|
||||||
"rm -rf " + mCachedFile.getParent() + "/*",
|
|
||||||
"rm -rf " + TMP_FOLDER_PATH
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (mCachedFile != null && mCachedFile.exists() && !mCachedFile.delete()) {
|
|
||||||
Utils.removeItem(mCachedFile.getPath());
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (Boolean.parseBoolean(ret.get(ret.size() - 1))) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -1 = error, manual install; 0 = invalid zip; 1 = success
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Integer result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
progress.dismiss();
|
|
||||||
switch (result) {
|
|
||||||
case -1:
|
|
||||||
Toast.makeText(mContext, mContext.getString(R.string.install_error), Toast.LENGTH_LONG).show();
|
|
||||||
Toast.makeText(mContext, mContext.getString(R.string.manual_install_1, mUri.getPath()), Toast.LENGTH_LONG).show();
|
|
||||||
Toast.makeText(mContext, mContext.getString(R.string.manual_install_2), Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case 0:
|
|
||||||
Toast.makeText(mContext, mContext.getString(R.string.invalid_zip), Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
onSuccess();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void onSuccess() {
|
|
||||||
StatusFragment.updateCheckDone.trigger();
|
|
||||||
new LoadModules().exec();
|
|
||||||
|
|
||||||
MainActivity.alertBuilder
|
|
||||||
.setTitle(R.string.reboot_title)
|
|
||||||
.setMessage(R.string.reboot_msg)
|
|
||||||
.setPositiveButton(R.string.reboot, (dialogInterface1, i) -> Shell.sh("su -c reboot"))
|
|
||||||
.setNegativeButton(R.string.no_thanks, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class MagiskHide extends RootTask<Object, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Object... params) {
|
|
||||||
boolean add = (boolean) params[0];
|
|
||||||
String packageName = (String) params[1];
|
|
||||||
Shell.su(MAGISK_HIDE_PATH + (add ? "add " : "rm ") + packageName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void add(CharSequence packageName) {
|
|
||||||
exec(true, packageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void rm(CharSequence packageName) {
|
|
||||||
exec(false, packageName);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class GetBootBlocks extends RootTask<Void, Void, Void> {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
if (Shell.rootAccess()) {
|
|
||||||
InstallFragment.blockList = Shell.su("ls /dev/block | grep mmc");
|
|
||||||
if (InstallFragment.bootBlock == null) {
|
|
||||||
InstallFragment.bootBlock = Utils.detectBootImage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Void aVoid) {
|
|
||||||
InstallFragment.blockDetectionDone.trigger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
public class ByteArrayInOutStream extends ByteArrayOutputStream {
|
||||||
|
public ByteArrayInputStream getInputStream() {
|
||||||
|
ByteArrayInputStream in = new ByteArrayInputStream(buf, 0, count);
|
||||||
|
count = 0;
|
||||||
|
buf = new byte[32];
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuffer(byte[] buffer) {
|
||||||
|
buf = buffer;
|
||||||
|
count = buffer.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public class CallbackEvent<Result> {
|
||||||
|
|
||||||
|
public boolean isTriggered = false;
|
||||||
|
private Result result;
|
||||||
|
private Set<Listener<Result>> listeners;
|
||||||
|
|
||||||
|
public void register(Listener<Result> l) {
|
||||||
|
if (listeners == null) {
|
||||||
|
listeners = new HashSet<>();
|
||||||
|
}
|
||||||
|
listeners.add(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unRegister() {
|
||||||
|
listeners = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unRegister(Listener<Result> l) {
|
||||||
|
if (listeners != null) {
|
||||||
|
listeners.remove(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void trigger() {
|
||||||
|
trigger(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void trigger(Result r) {
|
||||||
|
result = r;
|
||||||
|
isTriggered = true;
|
||||||
|
if (listeners != null) {
|
||||||
|
for (Listener<Result> listener : listeners) {
|
||||||
|
listener.onTrigger(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener<R> {
|
||||||
|
void onTrigger(CallbackEvent<R> event);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
public class CallbackHandler {
|
|
||||||
|
|
||||||
private static HashMap<Event, HashSet<EventListener>> listeners = new HashMap<>();
|
|
||||||
|
|
||||||
public static void register(Event event, EventListener listener) {
|
|
||||||
HashSet<EventListener> list = listeners.get(event);
|
|
||||||
if (list == null) {
|
|
||||||
list = new HashSet<>();
|
|
||||||
listeners.put(event, list);
|
|
||||||
}
|
|
||||||
list.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void unRegister(Event event, EventListener listener) {
|
|
||||||
HashSet<EventListener> list = listeners.get(event);
|
|
||||||
if (list != null) {
|
|
||||||
list.remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void triggerCallback(Event event) {
|
|
||||||
HashSet<EventListener> list = listeners.get(event);
|
|
||||||
if (list != null) {
|
|
||||||
for (EventListener listener : list) {
|
|
||||||
listener.onTrigger(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static class Event {
|
|
||||||
public boolean isTriggered = false;
|
|
||||||
public void trigger() {
|
|
||||||
isTriggered = true;
|
|
||||||
triggerCallback(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface EventListener {
|
|
||||||
void onTrigger(Event event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,37 +2,32 @@ package com.topjohnwu.magisk.utils;
|
|||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class Logger {
|
public class Logger {
|
||||||
|
|
||||||
public static final String TAG = "Magisk";
|
public static final String MAIN_TAG = "Magisk";
|
||||||
public static final String DEV_TAG = "Magisk: DEV";
|
public static final String DEBUG_TAG = "MagiskManager";
|
||||||
public static final String DEBUG_TAG = "Magisk: DEBUG";
|
|
||||||
|
|
||||||
public static boolean logShell, devLog;
|
public static void debug(String fmt, Object... args) {
|
||||||
|
Log.d(DEBUG_TAG, "DEBUG: " + String.format(Locale.US, fmt, args));
|
||||||
public static void debug(String msg) {
|
|
||||||
Log.d(DEBUG_TAG, msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void dev(String msg, Object... args) {
|
public static void error(String fmt, Object... args) {
|
||||||
if (devLog) {
|
Log.e(MAIN_TAG, "MANAGERERROR: " + String.format(Locale.US, fmt, args));
|
||||||
if (args.length == 1 && args[0] instanceof Throwable) {
|
}
|
||||||
Log.d(DEV_TAG, msg, (Throwable) args[0]);
|
|
||||||
} else {
|
public static void dev(String fmt, Object... args) {
|
||||||
Log.d(DEV_TAG, String.format(msg, args));
|
if (MagiskManager.devLogging) {
|
||||||
}
|
Log.d(DEBUG_TAG, String.format(Locale.US, fmt, args));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void dev(String msg) {
|
public static void shell(boolean root, String fmt, Object... args) {
|
||||||
if (devLog) {
|
if (MagiskManager.shellLogging) {
|
||||||
Log.d(DEV_TAG, msg);
|
Log.d(DEBUG_TAG, (root ? "MANAGERSU: " : "MANAGERSH: ") + String.format(Locale.US, fmt, args));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void shell(boolean root, String msg) {
|
|
||||||
if (logShell) {
|
|
||||||
Log.d(root ? "SU" : "SH", msg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
import com.topjohnwu.magisk.module.BaseModule;
|
|
||||||
import com.topjohnwu.magisk.module.Module;
|
|
||||||
import com.topjohnwu.magisk.module.Repo;
|
|
||||||
|
|
||||||
import org.json.JSONArray;
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ModuleHelper {
|
|
||||||
private static final String MAGISK_PATH = "/magisk";
|
|
||||||
private static final String FILE_KEY = "RepoMap";
|
|
||||||
private static final String REPO_KEY = "repomap";
|
|
||||||
private static final String VERSION_KEY = "version";
|
|
||||||
private static final int DATABASE_VER = 1;
|
|
||||||
|
|
||||||
private static ValueSortedMap<String, Repo> repoMap = new ValueSortedMap<>();
|
|
||||||
private static ValueSortedMap<String, Module> moduleMap = new ValueSortedMap<>();
|
|
||||||
|
|
||||||
|
|
||||||
public static void createModuleMap() {
|
|
||||||
Logger.dev("ModuleHelper: Loading modules");
|
|
||||||
|
|
||||||
moduleMap.clear();
|
|
||||||
|
|
||||||
for (String path : Utils.getModList(MAGISK_PATH)) {
|
|
||||||
Logger.dev("ModuleHelper: Adding modules from " + path);
|
|
||||||
Module module;
|
|
||||||
try {
|
|
||||||
module = new Module(path);
|
|
||||||
moduleMap.put(module.getId(), module);
|
|
||||||
} catch (BaseModule.CacheModException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.dev("ModuleHelper: Module load done");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void createRepoMap(Context context) {
|
|
||||||
Logger.dev("ModuleHelper: Loading repos");
|
|
||||||
|
|
||||||
repoMap.clear();
|
|
||||||
|
|
||||||
Gson gson = new Gson();
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(FILE_KEY, Context.MODE_PRIVATE);
|
|
||||||
String jsonString;
|
|
||||||
|
|
||||||
int cachedVersion = prefs.getInt(VERSION_KEY, 0);
|
|
||||||
if (cachedVersion != DATABASE_VER) {
|
|
||||||
// Ignore incompatible cached database
|
|
||||||
jsonString = null;
|
|
||||||
} else {
|
|
||||||
jsonString = prefs.getString(REPO_KEY, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
ValueSortedMap<String, Repo> cached = null;
|
|
||||||
|
|
||||||
if (jsonString != null) {
|
|
||||||
cached = gson.fromJson(jsonString, new TypeToken< ValueSortedMap<String, Repo> >(){}.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cached == null) {
|
|
||||||
cached = new ValueSortedMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Making a request to url and getting response
|
|
||||||
jsonString = WebRequest.makeWebServiceCall(context.getString(R.string.url_main, Utils.getToken()), WebRequest.GET);
|
|
||||||
|
|
||||||
if (jsonString != null && !jsonString.isEmpty()) {
|
|
||||||
// Have internet access
|
|
||||||
try {
|
|
||||||
JSONArray jsonArray = new JSONArray(jsonString);
|
|
||||||
for (int i = 0; i < jsonArray.length(); i++) {
|
|
||||||
JSONObject jsonobject = jsonArray.getJSONObject(i);
|
|
||||||
String id = jsonobject.getString("description");
|
|
||||||
String name = jsonobject.getString("name");
|
|
||||||
String lastUpdate = jsonobject.getString("pushed_at");
|
|
||||||
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
|
||||||
Date updatedDate;
|
|
||||||
try {
|
|
||||||
updatedDate = format.parse(lastUpdate);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Repo repo = cached.get(id);
|
|
||||||
try {
|
|
||||||
if (repo == null) {
|
|
||||||
Logger.dev("ModuleHelper: Create new repo " + id);
|
|
||||||
repo = new Repo(context, name, updatedDate);
|
|
||||||
} else {
|
|
||||||
Logger.dev("ModuleHelper: Cached repo " + id);
|
|
||||||
repo.update(updatedDate);
|
|
||||||
}
|
|
||||||
if (repo.getId() != null) {
|
|
||||||
repoMap.put(id, repo);
|
|
||||||
}
|
|
||||||
} catch (BaseModule.CacheModException ignored) {}
|
|
||||||
}
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use cached if no internet
|
|
||||||
repoMap.putAll(cached);
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(VERSION_KEY, DATABASE_VER)
|
|
||||||
.putString(REPO_KEY, gson.toJson(repoMap))
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
Logger.dev("ModuleHelper: Repo load done");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void getModuleList(List<Module> moduleList) {
|
|
||||||
moduleList.clear();
|
|
||||||
moduleList.addAll(moduleMap.values());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void getRepoLists(List<Repo> update, List<Repo> installed, List<Repo> others) {
|
|
||||||
update.clear();
|
|
||||||
installed.clear();
|
|
||||||
others.clear();
|
|
||||||
for (Repo repo : repoMap.values()) {
|
|
||||||
Module module = moduleMap.get(repo.getId());
|
|
||||||
if (module != null) {
|
|
||||||
if (repo.getVersionCode() > module.getVersionCode()) {
|
|
||||||
update.add(repo);
|
|
||||||
} else {
|
|
||||||
installed.add(repo);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
others.add(repo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class ValueSortedMap<K, V extends Comparable > extends HashMap<K, V> {
|
|
||||||
|
|
||||||
private List<V> sorted = new ArrayList<>();
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Collection<V> values() {
|
|
||||||
if (sorted.isEmpty()) {
|
|
||||||
sorted.addAll(super.values());
|
|
||||||
Collections.sort(sorted);
|
|
||||||
}
|
|
||||||
return sorted;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public V put(K key, V value) {
|
|
||||||
sorted.clear();
|
|
||||||
return super.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void putAll(Map<? extends K, ? extends V> m) {
|
|
||||||
sorted.clear();
|
|
||||||
super.putAll(m);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public V remove(Object key) {
|
|
||||||
sorted.clear();
|
|
||||||
return super.remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.util.Base64;
|
import android.util.Base64;
|
||||||
|
|
||||||
import com.google.android.gms.common.ConnectionResult;
|
import com.google.android.gms.common.ConnectionResult;
|
||||||
import com.google.android.gms.common.api.GoogleApiClient;
|
import com.google.android.gms.common.api.GoogleApiClient;
|
||||||
import com.google.android.gms.common.api.Status;
|
import com.google.android.gms.common.api.Status;
|
||||||
import com.google.android.gms.safetynet.SafetyNet;
|
import com.google.android.gms.safetynet.SafetyNet;
|
||||||
|
import com.topjohnwu.magisk.R;
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -19,38 +20,55 @@ import java.security.SecureRandom;
|
|||||||
public abstract class SafetyNetHelper
|
public abstract class SafetyNetHelper
|
||||||
implements GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks {
|
implements GoogleApiClient.OnConnectionFailedListener, GoogleApiClient.ConnectionCallbacks {
|
||||||
|
|
||||||
private GoogleApiClient mGoogleApiClient;
|
private static boolean isRunning = false;
|
||||||
|
|
||||||
public SafetyNetHelper(Context context) {
|
private GoogleApiClient mGoogleApiClient;
|
||||||
mGoogleApiClient = new GoogleApiClient.Builder(context)
|
private Result ret;
|
||||||
|
protected FragmentActivity mActivity;
|
||||||
|
|
||||||
|
public SafetyNetHelper(FragmentActivity activity) {
|
||||||
|
ret = new Result();
|
||||||
|
mActivity = activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry point to start test
|
||||||
|
public void requestTest() {
|
||||||
|
if (isRunning)
|
||||||
|
return;
|
||||||
|
// Connect Google Service
|
||||||
|
mGoogleApiClient = new GoogleApiClient.Builder(mActivity)
|
||||||
|
.enableAutoManage(mActivity, this)
|
||||||
.addApi(SafetyNet.API)
|
.addApi(SafetyNet.API)
|
||||||
.addConnectionCallbacks(this)
|
.addConnectionCallbacks(this)
|
||||||
.addOnConnectionFailedListener(this)
|
|
||||||
.build();
|
.build();
|
||||||
|
mGoogleApiClient.connect();
|
||||||
|
isRunning = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionFailed(@NonNull ConnectionResult result) {
|
public void onConnectionFailed(@NonNull ConnectionResult result) {
|
||||||
Logger.dev("SN: Google API fail");
|
Logger.dev("SN: Google API fail");
|
||||||
}
|
ret.errmsg = result.getErrorMessage();
|
||||||
|
handleResults(ret);
|
||||||
@Override
|
|
||||||
public void onConnected(@Nullable Bundle bundle) {
|
|
||||||
Logger.dev("SN: Google API Connected");
|
|
||||||
safetyNetCheck();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConnectionSuspended(int i) {
|
public void onConnectionSuspended(int i) {
|
||||||
Logger.dev("SN: Google API Suspended");
|
Logger.dev("SN: Google API Suspended");
|
||||||
|
switch (i) {
|
||||||
|
case CAUSE_NETWORK_LOST:
|
||||||
|
ret.errmsg = mActivity.getString(R.string.safetyNet_network_loss);
|
||||||
|
break;
|
||||||
|
case CAUSE_SERVICE_DISCONNECTED:
|
||||||
|
ret.errmsg = mActivity.getString(R.string.safetyNet_service_disconnected);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
handleResults(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void requestTest() {
|
@Override
|
||||||
// Connect Google Service
|
public void onConnected(@Nullable Bundle bundle) {
|
||||||
mGoogleApiClient.connect();
|
Logger.dev("SN: Google API Connected");
|
||||||
}
|
|
||||||
|
|
||||||
private void safetyNetCheck() {
|
|
||||||
// Create nonce
|
// Create nonce
|
||||||
byte[] nonce = new byte[24];
|
byte[] nonce = new byte[24];
|
||||||
new SecureRandom().nextBytes(nonce);
|
new SecureRandom().nextBytes(nonce);
|
||||||
@@ -66,16 +84,31 @@ public abstract class SafetyNetHelper
|
|||||||
Logger.dev("SN: Response: " + json);
|
Logger.dev("SN: Response: " + json);
|
||||||
try {
|
try {
|
||||||
JSONObject decoded = new JSONObject(json);
|
JSONObject decoded = new JSONObject(json);
|
||||||
handleResults(decoded.getBoolean("ctsProfileMatch") ? 1 : 0);
|
ret.ctsProfile = decoded.getBoolean("ctsProfileMatch");
|
||||||
} catch (JSONException ignored) {}
|
ret.basicIntegrity = decoded.getBoolean("basicIntegrity");
|
||||||
|
ret.failed = false;
|
||||||
|
} catch (JSONException e) {
|
||||||
|
ret.errmsg = mActivity.getString(R.string.safetyNet_res_invalid);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Logger.dev("SN: No response");
|
Logger.dev("SN: No response");
|
||||||
handleResults(-1);
|
ret.errmsg = mActivity.getString(R.string.safetyNet_no_response);
|
||||||
}
|
}
|
||||||
// Disconnect
|
// Disconnect
|
||||||
|
mGoogleApiClient.stopAutoManage(mActivity);
|
||||||
mGoogleApiClient.disconnect();
|
mGoogleApiClient.disconnect();
|
||||||
|
isRunning = false;
|
||||||
|
handleResults(ret);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract void handleResults(int i);
|
// Callback function to save the results
|
||||||
|
public abstract void handleResults(Result result);
|
||||||
|
|
||||||
|
public static class Result {
|
||||||
|
public boolean failed = true;
|
||||||
|
public String errmsg;
|
||||||
|
public boolean ctsProfile = false;
|
||||||
|
public boolean basicIntegrity = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.asyncs.RootTask;
|
||||||
|
|
||||||
import java.io.DataOutputStream;
|
import java.io.DataOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -14,17 +16,17 @@ public class Shell {
|
|||||||
|
|
||||||
// -1 = problematic/unknown issue; 0 = not rooted; 1 = properly rooted
|
// -1 = problematic/unknown issue; 0 = not rooted; 1 = properly rooted
|
||||||
public static int rootStatus;
|
public static int rootStatus;
|
||||||
|
public static final Object lock = new Object();
|
||||||
|
|
||||||
|
private static boolean isInit = false;
|
||||||
private static Process rootShell;
|
private static Process rootShell;
|
||||||
private static DataOutputStream rootSTDIN;
|
private static DataOutputStream rootSTDIN;
|
||||||
private static StreamGobbler rootSTDOUT;
|
private static StreamGobbler rootSTDOUT;
|
||||||
private static List<String> rootOutList = new ArrayList<>();
|
private static List<String> rootOutList = Collections.synchronizedList(new ArrayList<String>());
|
||||||
|
|
||||||
static {
|
public static void init() {
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void init() {
|
isInit = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
rootShell = Runtime.getRuntime().exec("su");
|
rootShell = Runtime.getRuntime().exec("su");
|
||||||
@@ -41,7 +43,6 @@ public class Shell {
|
|||||||
|
|
||||||
// Setup umask and PATH
|
// Setup umask and PATH
|
||||||
su("umask 022");
|
su("umask 022");
|
||||||
su("PATH=/data/busybox:$PATH");
|
|
||||||
|
|
||||||
List<String> ret = su("echo -BOC-", "id");
|
List<String> ret = su("echo -BOC-", "id");
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ public class Shell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean rootAccess() {
|
public static boolean rootAccess() {
|
||||||
return rootStatus > 0;
|
return isInit && rootStatus > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> sh(String... commands) {
|
public static List<String> sh(String... commands) {
|
||||||
@@ -120,7 +121,12 @@ public class Shell {
|
|||||||
DataOutputStream STDIN;
|
DataOutputStream STDIN;
|
||||||
StreamGobbler STDOUT;
|
StreamGobbler STDOUT;
|
||||||
|
|
||||||
if (!rootAccess()) {
|
// Create the default shell if not init
|
||||||
|
if (!newShell && !isInit) {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newShell && !rootAccess()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,6 +136,10 @@ public class Shell {
|
|||||||
process = Runtime.getRuntime().exec("su");
|
process = Runtime.getRuntime().exec("su");
|
||||||
STDIN = new DataOutputStream(process.getOutputStream());
|
STDIN = new DataOutputStream(process.getOutputStream());
|
||||||
STDOUT = new StreamGobbler(process.getInputStream(), res);
|
STDOUT = new StreamGobbler(process.getInputStream(), res);
|
||||||
|
|
||||||
|
// Run the new shell with busybox and proper umask
|
||||||
|
STDIN.write(("umask 022\n").getBytes("UTF-8"));
|
||||||
|
STDIN.flush();
|
||||||
} catch (IOException err) {
|
} catch (IOException err) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -170,6 +180,7 @@ public class Shell {
|
|||||||
try {
|
try {
|
||||||
// Process terminated, it means the interactive shell has some issues
|
// Process terminated, it means the interactive shell has some issues
|
||||||
process.exitValue();
|
process.exitValue();
|
||||||
|
rootStatus = -1;
|
||||||
return null;
|
return null;
|
||||||
} catch (IllegalThreadStateException e) {
|
} catch (IllegalThreadStateException e) {
|
||||||
// Process still running, gobble output until done
|
// Process still running, gobble output until done
|
||||||
@@ -183,20 +194,36 @@ public class Shell {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try { STDOUT.join(100); } catch (InterruptedException err) { return null; }
|
try { STDOUT.join(100); } catch (InterruptedException err) {
|
||||||
|
rootStatus = -1;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
if (!e.getMessage().contains("EPIPE")) {
|
if (!e.getMessage().contains("EPIPE")) {
|
||||||
Logger.dev("Shell: Root shell error...");
|
Logger.dev("Shell: Root shell error...");
|
||||||
|
rootStatus = -1;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} catch(InterruptedException e) {
|
} catch(InterruptedException e) {
|
||||||
Logger.dev("Shell: Root shell error...");
|
Logger.dev("Shell: Root shell error...");
|
||||||
|
rootStatus = -1;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ArrayList<>(res);
|
return new ArrayList<>(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void su_async(List<String> result, String... commands) {
|
||||||
|
new RootTask<Void, Void, Void>() {
|
||||||
|
@Override
|
||||||
|
protected Void doInRoot(Void... params) {
|
||||||
|
List<String> ret = Shell.su(commands);
|
||||||
|
if (result != null) result.addAll(ret);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}.exec();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,68 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
import android.app.DownloadManager;
|
import android.app.DownloadManager;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
|
import android.support.design.widget.Snackbar;
|
||||||
import android.support.v4.app.ActivityCompat;
|
import android.support.v4.app.ActivityCompat;
|
||||||
import android.util.Base64;
|
import android.support.v4.app.FragmentActivity;
|
||||||
|
import android.support.v4.app.TaskStackBuilder;
|
||||||
|
import android.support.v7.app.NotificationCompat;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.topjohnwu.magisk.MagiskManager;
|
||||||
import com.topjohnwu.magisk.R;
|
import com.topjohnwu.magisk.R;
|
||||||
|
import com.topjohnwu.magisk.SplashActivity;
|
||||||
|
import com.topjohnwu.magisk.asyncs.LoadRepos;
|
||||||
|
import com.topjohnwu.magisk.components.SnackbarMaker;
|
||||||
|
import com.topjohnwu.magisk.database.RepoDatabaseHelper;
|
||||||
import com.topjohnwu.magisk.receivers.DownloadReceiver;
|
import com.topjohnwu.magisk.receivers.DownloadReceiver;
|
||||||
|
import com.topjohnwu.magisk.receivers.ManagerUpdate;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.security.InvalidKeyException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.NoSuchPaddingException;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.DESKeySpec;
|
|
||||||
|
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
public static boolean isDownloading = false;
|
public static boolean isDownloading = false;
|
||||||
|
|
||||||
private static final String cryptoPass = "MagiskRox666";
|
private static final int MAGISK_UPDATE_NOTIFICATION_ID = 1;
|
||||||
private static final String secret = "GTYybRBTYf5his9kQ16ZNO7qgkBJ/5MyVe4CGceAOIoXgSnnk8FTd4F1dE9p5Eus";
|
private static final int APK_UPDATE_NOTIFICATION_ID = 2;
|
||||||
|
|
||||||
public static boolean itemExist(String path) {
|
public static boolean itemExist(String path) {
|
||||||
return itemExist(true, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean itemExist(boolean root, String path) {
|
|
||||||
String command = "if [ -e " + path + " ]; then echo true; else echo false; fi";
|
String command = "if [ -e " + path + " ]; then echo true; else echo false; fi";
|
||||||
if (Shell.rootAccess() && root) {
|
List<String> ret = Shell.su(command);
|
||||||
return Boolean.parseBoolean(Shell.su(command).get(0));
|
return isValidShellResponse(ret) && Boolean.parseBoolean(ret.get(0));
|
||||||
} else {
|
|
||||||
return new File(path).exists();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean commandExists(String s) {
|
public static void createFile(String path) {
|
||||||
List<String> ret;
|
|
||||||
String command = "if [ -z $(which " + s + ") ]; then echo false; else echo true; fi";
|
|
||||||
ret = Shell.sh(command);
|
|
||||||
return Boolean.parseBoolean(ret.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean createFile(String path) {
|
|
||||||
String folder = path.substring(0, path.lastIndexOf('/'));
|
String folder = path.substring(0, path.lastIndexOf('/'));
|
||||||
String command = "mkdir -p " + folder + " 2>/dev/null; touch " + path + " 2>/dev/null; if [ -f \"" + path + "\" ]; then echo true; else echo false; fi";
|
String command = "mkdir -p " + folder + " 2>/dev/null; touch " + path + " 2>/dev/null; if [ -f \"" + path + "\" ]; then echo true; else echo false; fi";
|
||||||
return Shell.rootAccess() && Boolean.parseBoolean(Shell.su(command).get(0));
|
Shell.su_async(null, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean removeItem(String path) {
|
public static void removeItem(String path) {
|
||||||
String command = "rm -rf " + path + " 2>/dev/null; if [ -e " + path + " ]; then echo false; else echo true; fi";
|
String command = "rm -rf " + path + " 2>/dev/null; if [ -e " + path + " ]; then echo false; else echo true; fi";
|
||||||
return Shell.rootAccess() && Boolean.parseBoolean(Shell.su(command).get(0));
|
Shell.su_async(null, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> getModList(String path) {
|
public static List<String> getModList(String path) {
|
||||||
List<String> ret;
|
|
||||||
String command = "find " + path + " -type d -maxdepth 1 ! -name \"*.core\" ! -name \"*lost+found\" ! -name \"*magisk\"";
|
String command = "find " + path + " -type d -maxdepth 1 ! -name \"*.core\" ! -name \"*lost+found\" ! -name \"*magisk\"";
|
||||||
if (Shell.rootAccess()) {
|
return Shell.su(command);
|
||||||
ret = Shell.su(command);
|
|
||||||
} else {
|
|
||||||
ret = Shell.sh(command);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> readFile(String path) {
|
public static List<String> readFile(String path) {
|
||||||
@@ -92,18 +77,19 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void dlAndReceive(Context context, DownloadReceiver receiver, String link, String filename) {
|
public static void dlAndReceive(Context context, DownloadReceiver receiver, String link, String filename) {
|
||||||
if (isDownloading) {
|
if (isDownloading)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file = new File(Environment.getExternalStorageDirectory() + "/MagiskManager/" + filename);
|
File file = new File(Environment.getExternalStorageDirectory() + "/MagiskManager/" + filename);
|
||||||
|
|
||||||
if ((!file.getParentFile().exists() && !file.getParentFile().mkdirs()) || (file.exists() && !file.delete())) {
|
if ((!file.getParentFile().exists() && !file.getParentFile().mkdirs())
|
||||||
|
|| (file.exists() && !file.delete())) {
|
||||||
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
|
Toast.makeText(context, R.string.permissionNotGranted, Toast.LENGTH_LONG).show();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -112,35 +98,14 @@ public class Utils {
|
|||||||
isDownloading = true;
|
isDownloading = true;
|
||||||
|
|
||||||
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
DownloadManager downloadManager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
|
||||||
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(link));
|
|
||||||
request.setDestinationUri(Uri.fromFile(file));
|
|
||||||
|
|
||||||
receiver.setDownloadID(downloadManager.enqueue(request));
|
if (link != null) {
|
||||||
receiver.setFilename(filename);
|
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(link));
|
||||||
context.registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
request.setDestinationUri(Uri.fromFile(file));
|
||||||
}
|
receiver.setDownloadID(downloadManager.enqueue(request));
|
||||||
|
|
||||||
public static String getToken() {
|
|
||||||
|
|
||||||
try {
|
|
||||||
DESKeySpec keySpec = new DESKeySpec(cryptoPass.getBytes("UTF8"));
|
|
||||||
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
|
|
||||||
SecretKey key = keyFactory.generateSecret(keySpec);
|
|
||||||
|
|
||||||
byte[] encrypedPwdBytes = Base64.decode(secret, Base64.DEFAULT);
|
|
||||||
// cipher is not thread safe
|
|
||||||
Cipher cipher = Cipher.getInstance("DES");
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, key);
|
|
||||||
byte[] decrypedValueBytes = (cipher.doFinal(encrypedPwdBytes));
|
|
||||||
|
|
||||||
return new String(decrypedValueBytes);
|
|
||||||
|
|
||||||
} catch (InvalidKeyException | UnsupportedEncodingException | NoSuchAlgorithmException
|
|
||||||
| BadPaddingException | NoSuchPaddingException | IllegalBlockSizeException
|
|
||||||
| InvalidKeySpecException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
return secret;
|
receiver.setFilename(filename);
|
||||||
|
context.getApplicationContext().registerReceiver(receiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getLegalFilename(CharSequence filename) {
|
public static String getLegalFilename(CharSequence filename) {
|
||||||
@@ -152,29 +117,135 @@ public class Utils {
|
|||||||
public static String detectBootImage() {
|
public static String detectBootImage() {
|
||||||
String[] commands = {
|
String[] commands = {
|
||||||
"for PARTITION in kern-a KERN-A android_boot ANDROID_BOOT kernel KERNEL boot BOOT lnx LNX; do",
|
"for PARTITION in kern-a KERN-A android_boot ANDROID_BOOT kernel KERNEL boot BOOT lnx LNX; do",
|
||||||
"BOOTIMAGE=`readlink /dev/block/by-name/$PARTITION || readlink /dev/block/platform/*/by-name/$PARTITION || readlink /dev/block/platform/*/*/by-name/$PARTITION`",
|
"BOOTIMAGE=`readlink /dev/block/by-name/$PARTITION || " +
|
||||||
|
"readlink /dev/block/platform/*/by-name/$PARTITION || " +
|
||||||
|
"readlink /dev/block/platform/*/*/by-name/$PARTITION`",
|
||||||
"if [ ! -z \"$BOOTIMAGE\" ]; then break; fi",
|
"if [ ! -z \"$BOOTIMAGE\" ]; then break; fi",
|
||||||
"done",
|
"done",
|
||||||
"echo \"${BOOTIMAGE##*/}\""
|
"echo \"$BOOTIMAGE\""
|
||||||
};
|
};
|
||||||
List<String> ret = Shell.su(commands);
|
List<String> ret = Shell.su(commands);
|
||||||
if (!ret.isEmpty()) {
|
if (isValidShellResponse(ret)) {
|
||||||
return ret.get(0);
|
return ret.get(0);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ByteArrayInOutStream extends ByteArrayOutputStream {
|
public static boolean lowercaseContains(CharSequence string, CharSequence nonNullLowercaseSearch) {
|
||||||
public ByteArrayInputStream getInputStream() {
|
return !TextUtils.isEmpty(string) && string.toString().toLowerCase().contains(nonNullLowercaseSearch);
|
||||||
ByteArrayInputStream in = new ByteArrayInputStream(buf, 0, count);
|
|
||||||
count = 0;
|
|
||||||
buf = new byte[32];
|
|
||||||
return in;
|
|
||||||
}
|
|
||||||
public void setBuffer(byte[] buffer) {
|
|
||||||
buf = buffer;
|
|
||||||
count = buffer.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isValidShellResponse(List<String> list) {
|
||||||
|
if (list != null && list.size() != 0) {
|
||||||
|
// Check if all empty
|
||||||
|
for (String res : list) {
|
||||||
|
if (!TextUtils.isEmpty(res)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPrefsInt(SharedPreferences prefs, String key, int def) {
|
||||||
|
return Integer.parseInt(prefs.getString(key, String.valueOf(def)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MagiskManager getMagiskManager(Context context) {
|
||||||
|
return (MagiskManager) context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void checkSafetyNet(FragmentActivity activity) {
|
||||||
|
new SafetyNetHelper(activity) {
|
||||||
|
@Override
|
||||||
|
public void handleResults(Result result) {
|
||||||
|
getMagiskManager(mActivity).SNCheckResult = result;
|
||||||
|
getMagiskManager(mActivity).safetyNetDone.trigger();
|
||||||
|
}
|
||||||
|
}.requestTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearRepoCache(Activity activity) {
|
||||||
|
MagiskManager magiskManager = getMagiskManager(activity);
|
||||||
|
magiskManager.prefs.edit().remove(LoadRepos.ETAG_KEY).apply();
|
||||||
|
new RepoDatabaseHelper(activity).clearRepo();
|
||||||
|
Toast.makeText(activity, R.string.repo_cache_cleared, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getNameFromUri(Context context, Uri uri) {
|
||||||
|
String name = null;
|
||||||
|
try (Cursor c = context.getContentResolver().query(uri, null, null, null, null)) {
|
||||||
|
if (c != null) {
|
||||||
|
int nameIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||||
|
if (nameIndex != -1) {
|
||||||
|
c.moveToFirst();
|
||||||
|
name = c.getString(nameIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name == null) {
|
||||||
|
int idx = uri.getPath().lastIndexOf('/');
|
||||||
|
name = uri.getPath().substring(idx + 1);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showUriSnack(Activity activity, Uri uri) {
|
||||||
|
SnackbarMaker.make(activity, activity.getString(R.string.internal_storage,
|
||||||
|
"/MagiskManager/" + Utils.getNameFromUri(activity, uri)),
|
||||||
|
Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.ok, (v)->{}).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean checkNetworkStatus(Context context) {
|
||||||
|
ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
|
||||||
|
return networkInfo != null && networkInfo.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean checkBits(int bits, int... masks) {
|
||||||
|
for (int mask : masks) {
|
||||||
|
if ((bits & mask) == 0)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showMagiskUpdate(MagiskManager magiskManager) {
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(magiskManager);
|
||||||
|
builder.setSmallIcon(R.drawable.ic_magisk)
|
||||||
|
.setContentTitle(magiskManager.getString(R.string.magisk_update_title))
|
||||||
|
.setContentText(magiskManager.getString(R.string.magisk_update_available, magiskManager.remoteMagiskVersionString))
|
||||||
|
.setChannelId(MagiskManager.NOTIFICATION_CHANNEL)
|
||||||
|
.setVibrate(new long[]{0, 100, 100, 100})
|
||||||
|
.setAutoCancel(true);
|
||||||
|
Intent intent = new Intent(magiskManager, SplashActivity.class);
|
||||||
|
intent.putExtra(MagiskManager.INTENT_SECTION, "install");
|
||||||
|
TaskStackBuilder stackBuilder = TaskStackBuilder.create(magiskManager);
|
||||||
|
stackBuilder.addParentStack(SplashActivity.class);
|
||||||
|
stackBuilder.addNextIntent(intent);
|
||||||
|
PendingIntent pendingIntent = stackBuilder.getPendingIntent(MAGISK_UPDATE_NOTIFICATION_ID,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
builder.setContentIntent(pendingIntent);
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) magiskManager.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
notificationManager.notify(MAGISK_UPDATE_NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showManagerUpdate(MagiskManager magiskManager) {
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(magiskManager);
|
||||||
|
builder.setSmallIcon(R.drawable.ic_magisk)
|
||||||
|
.setContentTitle(magiskManager.getString(R.string.manager_update_title))
|
||||||
|
.setContentText(magiskManager.getString(R.string.manager_download_install))
|
||||||
|
.setChannelId(MagiskManager.NOTIFICATION_CHANNEL)
|
||||||
|
.setVibrate(new long[]{0, 100, 100, 100})
|
||||||
|
.setAutoCancel(true);
|
||||||
|
Intent intent = new Intent(magiskManager, ManagerUpdate.class);
|
||||||
|
intent.putExtra("link", magiskManager.managerLink);
|
||||||
|
intent.putExtra("version", magiskManager.remoteManagerVersionString);
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getBroadcast(magiskManager,
|
||||||
|
APK_UPDATE_NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
builder.setContentIntent(pendingIntent);
|
||||||
|
NotificationManager notificationManager =
|
||||||
|
(NotificationManager) magiskManager.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
notificationManager.notify(APK_UPDATE_NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class ValueSortedMap<K, V extends Comparable<? super V>> extends HashMap<K, V> {
|
||||||
|
|
||||||
|
private List<V> sorted = new ArrayList<>();
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<V> values() {
|
||||||
|
if (sorted.isEmpty()) {
|
||||||
|
sorted.addAll(super.values());
|
||||||
|
Collections.sort(sorted);
|
||||||
|
}
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V put(K key, V value) {
|
||||||
|
sorted.clear();
|
||||||
|
return super.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void putAll(Map<? extends K, ? extends V> m) {
|
||||||
|
sorted.clear();
|
||||||
|
super.putAll(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public V remove(Object key) {
|
||||||
|
sorted.clear();
|
||||||
|
return super.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.net.HttpURLConnection;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import javax.net.ssl.HttpsURLConnection;
|
|
||||||
|
|
||||||
public class WebRequest {
|
|
||||||
|
|
||||||
static String response = null;
|
|
||||||
public final static int GET = 1;
|
|
||||||
public final static int POST = 2;
|
|
||||||
|
|
||||||
//Constructor with no parameter
|
|
||||||
public WebRequest() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Making web service call
|
|
||||||
*
|
|
||||||
* @url - url to make request
|
|
||||||
* @requestmethod - http request method
|
|
||||||
*/
|
|
||||||
public static String makeWebServiceCall(String url, int requestmethod) {
|
|
||||||
return makeWebServiceCall(url, requestmethod, null, false);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String makeWebServiceCall(String url, int requestmethod, boolean addNewLines) {
|
|
||||||
return makeWebServiceCall(url, requestmethod, null, addNewLines);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Making service call
|
|
||||||
*
|
|
||||||
* @url - url to make request
|
|
||||||
* @requestmethod - http request method
|
|
||||||
* @params - http request params
|
|
||||||
*/
|
|
||||||
public static String makeWebServiceCall(String urladdress, int requestmethod,
|
|
||||||
HashMap<String, String> params, boolean addNewLines) {
|
|
||||||
Logger.dev("WebRequest: Service call " + urladdress);
|
|
||||||
URL url;
|
|
||||||
String response = "";
|
|
||||||
try {
|
|
||||||
url = new URL(urladdress);
|
|
||||||
|
|
||||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
|
||||||
conn.setReadTimeout(15000);
|
|
||||||
conn.setConnectTimeout(15000);
|
|
||||||
conn.setDoInput(true);
|
|
||||||
|
|
||||||
if (requestmethod == POST) {
|
|
||||||
conn.setRequestMethod("POST");
|
|
||||||
} else if (requestmethod == GET) {
|
|
||||||
conn.setRequestMethod("GET");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params != null) {
|
|
||||||
OutputStream os = conn.getOutputStream();
|
|
||||||
BufferedWriter writer = new BufferedWriter(
|
|
||||||
new OutputStreamWriter(os, "UTF-8"));
|
|
||||||
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
boolean first = true;
|
|
||||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
|
||||||
if (first)
|
|
||||||
first = false;
|
|
||||||
else
|
|
||||||
result.append("&");
|
|
||||||
|
|
||||||
result.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
|
|
||||||
result.append("=");
|
|
||||||
result.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write(result.toString());
|
|
||||||
|
|
||||||
writer.flush();
|
|
||||||
writer.close();
|
|
||||||
os.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
int responseCode = conn.getResponseCode();
|
|
||||||
|
|
||||||
if (responseCode == HttpsURLConnection.HTTP_OK) {
|
|
||||||
String line;
|
|
||||||
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
||||||
while ((line = br.readLine()) != null) {
|
|
||||||
if (addNewLines) {
|
|
||||||
response += line + "\n";
|
|
||||||
} else {
|
|
||||||
response += line;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response = "";
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
92
app/src/main/java/com/topjohnwu/magisk/utils/WebService.java
Normal file
92
app/src/main/java/com/topjohnwu/magisk/utils/WebService.java
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.net.ssl.HttpsURLConnection;
|
||||||
|
|
||||||
|
public class WebService {
|
||||||
|
|
||||||
|
public final static int GET = 1;
|
||||||
|
public final static int POST = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Making web service call
|
||||||
|
*
|
||||||
|
* @url - url to make request
|
||||||
|
* @requestmethod - http request method
|
||||||
|
*/
|
||||||
|
public static String request(String url, int method) {
|
||||||
|
return request(url, method, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String request(String url, int method, boolean newline) {
|
||||||
|
return request(url, method, null, newline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Making service call
|
||||||
|
*
|
||||||
|
* @url - url to make request
|
||||||
|
* @requestmethod - http request method
|
||||||
|
* @params - http request params
|
||||||
|
* @header - http request header
|
||||||
|
* @newline - true to append a newline each line
|
||||||
|
*/
|
||||||
|
public static String request(String urlAddress, int method,
|
||||||
|
Map<String, String> header, boolean newline) {
|
||||||
|
Logger.dev("WebService: Service call " + urlAddress);
|
||||||
|
URL url;
|
||||||
|
StringBuilder response = new StringBuilder();
|
||||||
|
try {
|
||||||
|
url = new URL(urlAddress);
|
||||||
|
|
||||||
|
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||||
|
conn.setReadTimeout(15000);
|
||||||
|
conn.setConnectTimeout(15000);
|
||||||
|
conn.setDoInput(true);
|
||||||
|
|
||||||
|
if (method == POST) {
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
} else if (method == GET) {
|
||||||
|
conn.setRequestMethod("GET");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header != null) {
|
||||||
|
for (Map.Entry<String, String> entry : header.entrySet()) {
|
||||||
|
conn.setRequestProperty(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = conn.getResponseCode();
|
||||||
|
|
||||||
|
if (responseCode == HttpsURLConnection.HTTP_OK) {
|
||||||
|
String line;
|
||||||
|
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||||
|
while ((line = br.readLine()) != null) {
|
||||||
|
if (newline) {
|
||||||
|
response.append(line).append("\n");
|
||||||
|
} else {
|
||||||
|
response.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (header != null) {
|
||||||
|
header.clear();
|
||||||
|
for (Map.Entry<String, List<String>> entry : conn.getHeaderFields().entrySet()) {
|
||||||
|
List<String> l = entry.getValue();
|
||||||
|
header.put(entry.getKey(), l.get(l.size() - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
import android.webkit.WebResourceRequest;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
import android.webkit.WebViewClient;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.R;
|
|
||||||
|
|
||||||
public class WebWindow {
|
|
||||||
|
|
||||||
public WebWindow(String title, String url, Context context) {
|
|
||||||
AlertDialog.Builder alert;
|
|
||||||
String theme = PreferenceManager.getDefaultSharedPreferences(context).getString("theme", "");
|
|
||||||
if (theme.equals("Dark")) {
|
|
||||||
alert = new AlertDialog.Builder(context, R.style.AlertDialog_dh);
|
|
||||||
} else {
|
|
||||||
alert = new AlertDialog.Builder(context);
|
|
||||||
}
|
|
||||||
alert.setTitle(title);
|
|
||||||
|
|
||||||
Logger.dev("WebView: URL = " + url);
|
|
||||||
|
|
||||||
WebView wv = new WebView(context);
|
|
||||||
wv.loadUrl(url);
|
|
||||||
wv.setWebViewClient(new WebViewClient() {
|
|
||||||
@Override
|
|
||||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
|
||||||
view.loadUrl(url);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
alert.setView(wv);
|
|
||||||
alert.setNegativeButton("Close", (dialog, id) -> dialog.dismiss());
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
package com.topjohnwu.magisk.utils;
|
package com.topjohnwu.magisk.utils;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Pair;
|
|
||||||
|
|
||||||
import com.topjohnwu.magisk.utils.Utils.ByteArrayInOutStream;
|
|
||||||
|
|
||||||
import org.spongycastle.asn1.ASN1InputStream;
|
import org.spongycastle.asn1.ASN1InputStream;
|
||||||
import org.spongycastle.asn1.ASN1ObjectIdentifier;
|
import org.spongycastle.asn1.ASN1ObjectIdentifier;
|
||||||
import org.spongycastle.asn1.DEROutputStream;
|
import org.spongycastle.asn1.DEROutputStream;
|
||||||
import org.spongycastle.asn1.cms.CMSObjectIdentifiers;
|
import org.spongycastle.asn1.cms.CMSObjectIdentifiers;
|
||||||
|
import org.spongycastle.asn1.pkcs.PrivateKeyInfo;
|
||||||
import org.spongycastle.cert.jcajce.JcaCertStore;
|
import org.spongycastle.cert.jcajce.JcaCertStore;
|
||||||
import org.spongycastle.cms.CMSException;
|
import org.spongycastle.cms.CMSException;
|
||||||
import org.spongycastle.cms.CMSProcessableByteArray;
|
import org.spongycastle.cms.CMSProcessableByteArray;
|
||||||
@@ -23,8 +21,10 @@ import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
|
|||||||
import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||||
import org.spongycastle.util.encoders.Base64;
|
import org.spongycastle.util.encoders.Base64;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.FilterOutputStream;
|
import java.io.FilterOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -33,18 +33,14 @@ import java.io.OutputStream;
|
|||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.security.DigestOutputStream;
|
import java.security.DigestOutputStream;
|
||||||
import java.security.GeneralSecurityException;
|
import java.security.GeneralSecurityException;
|
||||||
import java.security.Key;
|
|
||||||
import java.security.KeyFactory;
|
import java.security.KeyFactory;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
import java.security.PrivateKey;
|
||||||
import java.security.Provider;
|
import java.security.Provider;
|
||||||
import java.security.Security;
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.KeySpec;
|
|
||||||
import java.security.spec.PKCS8EncodedKeySpec;
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -61,39 +57,35 @@ import java.util.jar.JarOutputStream;
|
|||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
/*
|
||||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
* Modified from from AOSP(Marshmallow) SignAPK.java
|
||||||
import javax.crypto.SecretKeyFactory;
|
* */
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
|
|
||||||
public class ZipUtils {
|
public class ZipUtils {
|
||||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
// File name in assets
|
||||||
private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
|
|
||||||
private static final String OTACERT_NAME = "META-INF/com/android/otacert";
|
|
||||||
private static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem";
|
private static final String PUBLIC_KEY_NAME = "public.certificate.x509.pem";
|
||||||
private static final String PRIVATE_KEY_NAME = "private.key.pk8";
|
private static final String PRIVATE_KEY_NAME = "private.key.pk8";
|
||||||
|
|
||||||
|
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
||||||
|
private static final String CERT_SIG_NAME = "META-INF/CERT.%s";
|
||||||
|
|
||||||
private static Provider sBouncyCastleProvider;
|
private static Provider sBouncyCastleProvider;
|
||||||
// bitmasks for which hash algorithms we need the manifest to include.
|
// bitmasks for which hash algorithms we need the manifest to include.
|
||||||
private static final int USE_SHA1 = 1;
|
private static final int USE_SHA1 = 1;
|
||||||
private static final int USE_SHA256 = 2;
|
private static final int USE_SHA256 = 2;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("zipadjust");
|
|
||||||
sBouncyCastleProvider = new BouncyCastleProvider();
|
sBouncyCastleProvider = new BouncyCastleProvider();
|
||||||
Security.insertProviderAt(sBouncyCastleProvider, 1);
|
Security.insertProviderAt(sBouncyCastleProvider, 1);
|
||||||
|
System.loadLibrary("zipadjust");
|
||||||
}
|
}
|
||||||
|
|
||||||
public native static byte[] zipAdjust(byte[] bytes, int size);
|
public native static void zipAdjust(String filenameIn, String filenameOut);
|
||||||
|
|
||||||
// Wrapper function for the JNI function
|
public static void removeTopFolder(InputStream in, File output) throws IOException {
|
||||||
public static void adjustZip(ByteArrayInOutStream buffer) {
|
|
||||||
buffer.setBuffer(zipAdjust(buffer.toByteArray(), buffer.size()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void removeTopFolder(InputStream in, OutputStream out) {
|
|
||||||
try {
|
try {
|
||||||
JarInputStream source = new JarInputStream(in);
|
JarInputStream source = new JarInputStream(in);
|
||||||
JarOutputStream dest = new JarOutputStream(out);
|
JarOutputStream dest = new JarOutputStream(new FileOutputStream(output));
|
||||||
JarEntry entry;
|
JarEntry entry;
|
||||||
String path;
|
String path;
|
||||||
int size;
|
int size;
|
||||||
@@ -102,42 +94,39 @@ public class ZipUtils {
|
|||||||
// Remove the top directory from the path
|
// Remove the top directory from the path
|
||||||
path = entry.getName().substring(entry.getName().indexOf("/") + 1);
|
path = entry.getName().substring(entry.getName().indexOf("/") + 1);
|
||||||
// If it's the top folder, ignore it
|
// If it's the top folder, ignore it
|
||||||
if (path.isEmpty())
|
if (path.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
// Don't include placeholder
|
// Don't include placeholder
|
||||||
if (path.contains("system/placeholder"))
|
if (path.contains("system/placeholder")) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
dest.putNextEntry(new JarEntry(path));
|
dest.putNextEntry(new JarEntry(path));
|
||||||
while((size = source.read(buffer, 0, 2048)) != -1)
|
while((size = source.read(buffer, 0, 2048)) != -1) {
|
||||||
dest.write(buffer, 0, size);
|
dest.write(buffer, 0, size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
source.close();
|
source.close();
|
||||||
dest.close();
|
dest.close();
|
||||||
in.close();
|
in.close();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
|
||||||
Logger.dev("ZipUtils: removeTopFolder IO error!");
|
Logger.dev("ZipUtils: removeTopFolder IO error!");
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void unzip(File file, File folder) {
|
public static void unzip(File file, File folder, String path) throws Exception {
|
||||||
unzip(file, folder, "");
|
int count;
|
||||||
}
|
FileOutputStream out;
|
||||||
|
File dest;
|
||||||
public static void unzip(File file, File folder, String path) {
|
InputStream is;
|
||||||
try {
|
JarEntry entry;
|
||||||
int count;
|
byte data[] = new byte[4096];
|
||||||
FileOutputStream out;
|
try (JarFile zipfile = new JarFile(file)) {
|
||||||
File dest;
|
Enumeration<JarEntry> e = zipfile.entries();
|
||||||
InputStream is;
|
|
||||||
JarEntry entry;
|
|
||||||
byte data[] = new byte[4096];
|
|
||||||
JarFile zipfile = new JarFile(file);
|
|
||||||
Enumeration e = zipfile.entries();
|
|
||||||
while(e.hasMoreElements()) {
|
while(e.hasMoreElements()) {
|
||||||
entry = (JarEntry) e.nextElement();
|
entry = e.nextElement();
|
||||||
if (!entry.getName().contains(path)
|
if (!entry.getName().contains(path) || entry.isDirectory()){
|
||||||
|| entry.getName().charAt(entry.getName().length() - 1) == '/') {
|
|
||||||
// Ignore directories, only create files
|
// Ignore directories, only create files
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -148,7 +137,7 @@ public class ZipUtils {
|
|||||||
dest.createNewFile();
|
dest.createNewFile();
|
||||||
}
|
}
|
||||||
out = new FileOutputStream(dest);
|
out = new FileOutputStream(dest);
|
||||||
while ((count = is.read(data, 0, 4096)) != -1) {
|
while ((count = is.read(data)) != -1) {
|
||||||
out.write(data, 0, count);
|
out.write(data, 0, count);
|
||||||
}
|
}
|
||||||
out.flush();
|
out.flush();
|
||||||
@@ -157,31 +146,31 @@ public class ZipUtils {
|
|||||||
}
|
}
|
||||||
} catch(Exception e) {
|
} catch(Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void signZip(Context context, InputStream inputStream,
|
public static void signZip(Context context, File input, File output, boolean minSign) {
|
||||||
OutputStream outputStream, boolean signWholeFile) {
|
int alignment = 4;
|
||||||
JarMap inputJar;
|
JarFile inputJar = null;
|
||||||
|
FileOutputStream outputFile = null;
|
||||||
int hashes = 0;
|
int hashes = 0;
|
||||||
try {
|
try {
|
||||||
X509Certificate publicKey = readPublicKey(context.getAssets().open(PUBLIC_KEY_NAME));
|
X509Certificate publicKey = readPublicKey(context.getAssets().open(PUBLIC_KEY_NAME));
|
||||||
hashes |= getDigestAlgorithm(publicKey);
|
hashes |= getDigestAlgorithm(publicKey);
|
||||||
|
|
||||||
// Set the ZIP file timestamp to the starting valid time
|
// Set the ZIP file timestamp to the starting valid time
|
||||||
// of the 0th certificate plus one hour (to match what
|
// of the 0th certificate plus one hour (to match what
|
||||||
// we've historically done).
|
// we've historically done).
|
||||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
||||||
PrivateKey privateKey = readPrivateKey(context.getAssets().open(PRIVATE_KEY_NAME));
|
PrivateKey privateKey = readPrivateKey(context.getAssets().open(PRIVATE_KEY_NAME));
|
||||||
inputJar = new JarMap(new JarInputStream(inputStream));
|
|
||||||
if (signWholeFile) {
|
outputFile = new FileOutputStream(output);
|
||||||
if (!"RSA".equalsIgnoreCase(privateKey.getAlgorithm())) {
|
if (minSign) {
|
||||||
System.err.println("Cannot sign OTA packages with non-RSA keys");
|
ZipUtils.signWholeFile(input, publicKey, privateKey, outputFile);
|
||||||
System.exit(1);
|
|
||||||
}
|
|
||||||
signWholeFile(inputJar, context.getAssets().open(PUBLIC_KEY_NAME),
|
|
||||||
publicKey, privateKey, outputStream);
|
|
||||||
} else {
|
} else {
|
||||||
JarOutputStream outputJar = new JarOutputStream(outputStream);
|
inputJar = new JarFile(input, false); // Don't verify.
|
||||||
|
JarOutputStream outputJar = new JarOutputStream(outputFile);
|
||||||
// For signing .apks, use the maximum compression to make
|
// For signing .apks, use the maximum compression to make
|
||||||
// them as small as possible (since they live forever on
|
// them as small as possible (since they live forever on
|
||||||
// the system partition). For OTA packages, use the
|
// the system partition). For OTA packages, use the
|
||||||
@@ -190,49 +179,23 @@ public class ZipUtils {
|
|||||||
// (~0.1% on full OTA packages I tested).
|
// (~0.1% on full OTA packages I tested).
|
||||||
outputJar.setLevel(9);
|
outputJar.setLevel(9);
|
||||||
Manifest manifest = addDigestsToManifest(inputJar, hashes);
|
Manifest manifest = addDigestsToManifest(inputJar, hashes);
|
||||||
copyFiles(manifest, inputJar, outputJar, timestamp);
|
copyFiles(manifest, inputJar, outputJar, timestamp, alignment);
|
||||||
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
|
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
|
||||||
outputJar.close();
|
outputJar.close();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
} finally {
|
||||||
}
|
try {
|
||||||
|
if (inputJar != null) inputJar.close();
|
||||||
public static class JarMap extends TreeMap<String, Pair<JarEntry, ByteArrayOutputStream> > {
|
if (outputFile != null) outputFile.close();
|
||||||
|
} catch (IOException e) {
|
||||||
private Manifest manifest;
|
e.printStackTrace();
|
||||||
|
|
||||||
public JarMap(JarInputStream in) throws IOException {
|
|
||||||
super();
|
|
||||||
manifest = in.getManifest();
|
|
||||||
byte[] buffer = new byte[4096];
|
|
||||||
int num;
|
|
||||||
JarEntry entry;
|
|
||||||
while ((entry = in.getNextJarEntry()) != null) {
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
||||||
while ((num = in.read(buffer)) > 0) {
|
|
||||||
stream.write(buffer, 0, num);
|
|
||||||
}
|
|
||||||
put(entry.getName(), entry, stream);
|
|
||||||
}
|
}
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public JarEntry getJarEntry(String name) {
|
|
||||||
return get(name).first;
|
|
||||||
}
|
|
||||||
public ByteArrayOutputStream getStream(String name) {
|
|
||||||
return get(name).second;
|
|
||||||
}
|
|
||||||
public void put(String name, JarEntry entry, ByteArrayOutputStream stream) {
|
|
||||||
put(name, new Pair<>(entry, stream));
|
|
||||||
}
|
|
||||||
public Manifest getManifest() {
|
|
||||||
return manifest;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return one of USE_SHA1 or USE_SHA256 according to the signature
|
* Return one of USE_SHA1 or USE_SHA256 according to the signature
|
||||||
* algorithm specified in the cert.
|
* algorithm specified in the cert.
|
||||||
@@ -259,8 +222,6 @@ public class ZipUtils {
|
|||||||
} else {
|
} else {
|
||||||
return "SHA1withRSA";
|
return "SHA1withRSA";
|
||||||
}
|
}
|
||||||
} else if ("DSA".equalsIgnoreCase(keyType)) {
|
|
||||||
return "SHA256withDSA";
|
|
||||||
} else if ("EC".equalsIgnoreCase(keyType)) {
|
} else if ("EC".equalsIgnoreCase(keyType)) {
|
||||||
return "SHA256withECDSA";
|
return "SHA256withECDSA";
|
||||||
} else {
|
} else {
|
||||||
@@ -281,79 +242,32 @@ public class ZipUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Read a PKCS#8 format private key. */
|
||||||
* Decrypt an encrypted PKCS 8 format private key.
|
|
||||||
*
|
|
||||||
* Based on ghstark's post on Aug 6, 2006 at
|
|
||||||
* http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949
|
|
||||||
*
|
|
||||||
* @param encryptedPrivateKey The raw data of the private key
|
|
||||||
* @param keyFile The file containing the private key
|
|
||||||
*/
|
|
||||||
private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile)
|
|
||||||
throws GeneralSecurityException {
|
|
||||||
EncryptedPrivateKeyInfo epkInfo;
|
|
||||||
try {
|
|
||||||
epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
// Probably not an encrypted key.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// We no longer have console, so need to use another way to input password
|
|
||||||
// This function is left here if needed in the future, so no use for now
|
|
||||||
char[] password = new char[0];
|
|
||||||
SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName());
|
|
||||||
Key key = skFactory.generateSecret(new PBEKeySpec(password));
|
|
||||||
Cipher cipher = Cipher.getInstance(epkInfo.getAlgName());
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters());
|
|
||||||
try {
|
|
||||||
return epkInfo.getKeySpec(cipher);
|
|
||||||
} catch (InvalidKeySpecException ex) {
|
|
||||||
System.err.println("signapk: Password for " + keyFile + " may be bad.");
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Read a PKCS 8 format private key. */
|
|
||||||
private static PrivateKey readPrivateKey(InputStream input)
|
private static PrivateKey readPrivateKey(InputStream input)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
try {
|
try {
|
||||||
byte[] buffer = new byte[4096];
|
byte[] buffer = new byte[4096];
|
||||||
int size = input.read(buffer);
|
int size = input.read(buffer);
|
||||||
byte[] bytes = Arrays.copyOf(buffer, size);
|
byte[] bytes = Arrays.copyOf(buffer, size);
|
||||||
KeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
/* Check to see if this is in an EncryptedPrivateKeyInfo structure. */
|
||||||
PrivateKey key;
|
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
|
||||||
key = decodeAsKeyType(spec, "RSA");
|
/*
|
||||||
if (key != null) {
|
* Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
|
||||||
return key;
|
* OID and use that to construct a KeyFactory.
|
||||||
}
|
*/
|
||||||
key = decodeAsKeyType(spec, "DSA");
|
ASN1InputStream bIn = new ASN1InputStream(new ByteArrayInputStream(spec.getEncoded()));
|
||||||
if (key != null) {
|
PrivateKeyInfo pki = PrivateKeyInfo.getInstance(bIn.readObject());
|
||||||
return key;
|
String algOid = pki.getPrivateKeyAlgorithm().getAlgorithm().getId();
|
||||||
}
|
return KeyFactory.getInstance(algOid).generatePrivate(spec);
|
||||||
key = decodeAsKeyType(spec, "EC");
|
|
||||||
if (key != null) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
throw new NoSuchAlgorithmException("Must be an RSA, DSA, or EC key");
|
|
||||||
} finally {
|
} finally {
|
||||||
input.close();
|
input.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private static PrivateKey decodeAsKeyType(KeySpec spec, String keyType)
|
|
||||||
throws GeneralSecurityException {
|
|
||||||
try {
|
|
||||||
return KeyFactory.getInstance(keyType).generatePrivate(spec);
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the hash(es) of every file to the manifest, creating it if
|
* Add the hash(es) of every file to the manifest, creating it if
|
||||||
* necessary.
|
* necessary.
|
||||||
*/
|
*/
|
||||||
private static Manifest addDigestsToManifest(JarMap jar, int hashes)
|
private static Manifest addDigestsToManifest(JarFile jar, int hashes)
|
||||||
throws IOException, GeneralSecurityException {
|
throws IOException, GeneralSecurityException {
|
||||||
Manifest input = jar.getManifest();
|
Manifest input = jar.getManifest();
|
||||||
Manifest output = new Manifest();
|
Manifest output = new Manifest();
|
||||||
@@ -372,17 +286,25 @@ public class ZipUtils {
|
|||||||
if ((hashes & USE_SHA256) != 0) {
|
if ((hashes & USE_SHA256) != 0) {
|
||||||
md_sha256 = MessageDigest.getInstance("SHA256");
|
md_sha256 = MessageDigest.getInstance("SHA256");
|
||||||
}
|
}
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int num;
|
||||||
// We sort the input entries by name, and add them to the
|
// We sort the input entries by name, and add them to the
|
||||||
// output manifest in sorted order. We expect that the output
|
// output manifest in sorted order. We expect that the output
|
||||||
// map will be deterministic.
|
// map will be deterministic.
|
||||||
/* JarMap is a TreeMap, so it's already sorted */
|
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
|
||||||
for (String name : jar.keySet()) {
|
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
|
||||||
JarEntry entry = jar.getJarEntry(name);
|
JarEntry entry = e.nextElement();
|
||||||
|
byName.put(entry.getName(), entry);
|
||||||
|
}
|
||||||
|
for (JarEntry entry: byName.values()) {
|
||||||
|
String name = entry.getName();
|
||||||
if (!entry.isDirectory() &&
|
if (!entry.isDirectory() &&
|
||||||
(stripPattern == null || !stripPattern.matcher(name).matches())) {
|
(stripPattern == null || !stripPattern.matcher(name).matches())) {
|
||||||
byte[] buffer = jar.getStream(name).toByteArray();
|
InputStream data = jar.getInputStream(entry);
|
||||||
if (md_sha1 != null) md_sha1.update(buffer, 0, buffer.length);
|
while ((num = data.read(buffer)) > 0) {
|
||||||
if (md_sha256 != null) md_sha256.update(buffer, 0, buffer.length);
|
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
|
||||||
|
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
|
||||||
|
}
|
||||||
Attributes attr = null;
|
Attributes attr = null;
|
||||||
if (input != null) attr = input.getAttributes(name);
|
if (input != null) attr = input.getAttributes(name);
|
||||||
attr = attr != null ? new Attributes(attr) : new Attributes();
|
attr = attr != null ? new Attributes(attr) : new Attributes();
|
||||||
@@ -400,36 +322,6 @@ public class ZipUtils {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a copy of the public key to the archive; this should
|
|
||||||
* exactly match one of the files in
|
|
||||||
* /system/etc/security/otacerts.zip on the device. (The same
|
|
||||||
* cert can be extracted from the CERT.RSA file but this is much
|
|
||||||
* easier to get at.)
|
|
||||||
*/
|
|
||||||
private static void addOtacert(JarOutputStream outputJar,
|
|
||||||
InputStream input,
|
|
||||||
long timestamp,
|
|
||||||
Manifest manifest,
|
|
||||||
int hash)
|
|
||||||
throws IOException, GeneralSecurityException {
|
|
||||||
MessageDigest md = MessageDigest.getInstance(hash == USE_SHA1 ? "SHA1" : "SHA256");
|
|
||||||
JarEntry je = new JarEntry(OTACERT_NAME);
|
|
||||||
je.setTime(timestamp);
|
|
||||||
outputJar.putNextEntry(je);
|
|
||||||
byte[] b = new byte[4096];
|
|
||||||
int read;
|
|
||||||
while ((read = input.read(b)) != -1) {
|
|
||||||
outputJar.write(b, 0, read);
|
|
||||||
md.update(b, 0, read);
|
|
||||||
}
|
|
||||||
input.close();
|
|
||||||
Attributes attr = new Attributes();
|
|
||||||
attr.putValue(hash == USE_SHA1 ? "SHA1-Digest" : "SHA-256-Digest",
|
|
||||||
new String(Base64.encode(md.digest()), "ASCII"));
|
|
||||||
manifest.getEntries().put(OTACERT_NAME, attr);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Write to another stream and track how many bytes have been
|
/** Write to another stream and track how many bytes have been
|
||||||
* written.
|
* written.
|
||||||
*/
|
*/
|
||||||
@@ -453,7 +345,6 @@ public class ZipUtils {
|
|||||||
return mCount;
|
return mCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Write a .SF file with a digest of the specified manifest. */
|
/** Write a .SF file with a digest of the specified manifest. */
|
||||||
private static void writeSignatureFile(Manifest manifest, OutputStream out,
|
private static void writeSignatureFile(Manifest manifest, OutputStream out,
|
||||||
int hash)
|
int hash)
|
||||||
@@ -497,12 +388,15 @@ public class ZipUtils {
|
|||||||
cout.write('\n');
|
cout.write('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Sign data and write the digital signature to 'out'. */
|
/** Sign data and write the digital signature to 'out'. */
|
||||||
private static void writeSignatureBlock(
|
private static void writeSignatureBlock(
|
||||||
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey, OutputStream out)
|
CMSTypedData data, X509Certificate publicKey, PrivateKey privateKey,
|
||||||
throws IOException, CertificateEncodingException, OperatorCreationException, CMSException {
|
OutputStream out)
|
||||||
ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(1);
|
throws IOException,
|
||||||
|
CertificateEncodingException,
|
||||||
|
OperatorCreationException,
|
||||||
|
CMSException {
|
||||||
|
ArrayList<X509Certificate> certList = new ArrayList<>(1);
|
||||||
certList.add(publicKey);
|
certList.add(publicKey);
|
||||||
JcaCertStore certs = new JcaCertStore(certList);
|
JcaCertStore certs = new JcaCertStore(certList);
|
||||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||||
@@ -528,141 +422,124 @@ public class ZipUtils {
|
|||||||
* reduce variation in the output file and make incremental OTAs
|
* reduce variation in the output file and make incremental OTAs
|
||||||
* more efficient.
|
* more efficient.
|
||||||
*/
|
*/
|
||||||
private static void copyFiles(Manifest manifest,
|
private static void copyFiles(Manifest manifest, JarFile in, JarOutputStream out,
|
||||||
JarMap in, JarOutputStream out, long timestamp) throws IOException {
|
long timestamp, int alignment) throws IOException {
|
||||||
|
byte[] buffer = new byte[4096];
|
||||||
|
int num;
|
||||||
Map<String, Attributes> entries = manifest.getEntries();
|
Map<String, Attributes> entries = manifest.getEntries();
|
||||||
ArrayList<String> names = new ArrayList<>(entries.keySet());
|
ArrayList<String> names = new ArrayList<String>(entries.keySet());
|
||||||
Collections.sort(names);
|
Collections.sort(names);
|
||||||
|
boolean firstEntry = true;
|
||||||
|
long offset = 0L;
|
||||||
|
// We do the copy in two passes -- first copying all the
|
||||||
|
// entries that are STORED, then copying all the entries that
|
||||||
|
// have any other compression flag (which in practice means
|
||||||
|
// DEFLATED). This groups all the stored entries together at
|
||||||
|
// the start of the file and makes it easier to do alignment
|
||||||
|
// on them (since only stored entries are aligned).
|
||||||
for (String name : names) {
|
for (String name : names) {
|
||||||
JarEntry inEntry = in.getJarEntry(name);
|
JarEntry inEntry = in.getJarEntry(name);
|
||||||
JarEntry outEntry;
|
JarEntry outEntry = null;
|
||||||
if (inEntry.getMethod() == JarEntry.STORED) {
|
if (inEntry.getMethod() != JarEntry.STORED) continue;
|
||||||
// Preserve the STORED method of the input entry.
|
// Preserve the STORED method of the input entry.
|
||||||
outEntry = new JarEntry(inEntry);
|
outEntry = new JarEntry(inEntry);
|
||||||
} else {
|
outEntry.setTime(timestamp);
|
||||||
// Create a new entry so that the compressed len is recomputed.
|
// 'offset' is the offset into the file at which we expect
|
||||||
outEntry = new JarEntry(name);
|
// the file data to begin. This is the value we need to
|
||||||
|
// make a multiple of 'alignement'.
|
||||||
|
offset += JarFile.LOCHDR + outEntry.getName().length();
|
||||||
|
if (firstEntry) {
|
||||||
|
// The first entry in a jar file has an extra field of
|
||||||
|
// four bytes that you can't get rid of; any extra
|
||||||
|
// data you specify in the JarEntry is appended to
|
||||||
|
// these forced four bytes. This is JAR_MAGIC in
|
||||||
|
// JarOutputStream; the bytes are 0xfeca0000.
|
||||||
|
offset += 4;
|
||||||
|
firstEntry = false;
|
||||||
}
|
}
|
||||||
|
if (alignment > 0 && (offset % alignment != 0)) {
|
||||||
|
// Set the "extra data" of the entry to between 1 and
|
||||||
|
// alignment-1 bytes, to make the file data begin at
|
||||||
|
// an aligned offset.
|
||||||
|
int needed = alignment - (int)(offset % alignment);
|
||||||
|
outEntry.setExtra(new byte[needed]);
|
||||||
|
offset += needed;
|
||||||
|
}
|
||||||
|
out.putNextEntry(outEntry);
|
||||||
|
InputStream data = in.getInputStream(inEntry);
|
||||||
|
while ((num = data.read(buffer)) > 0) {
|
||||||
|
out.write(buffer, 0, num);
|
||||||
|
offset += num;
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
// Copy all the non-STORED entries. We don't attempt to
|
||||||
|
// maintain the 'offset' variable past this point; we don't do
|
||||||
|
// alignment on these entries.
|
||||||
|
for (String name : names) {
|
||||||
|
JarEntry inEntry = in.getJarEntry(name);
|
||||||
|
JarEntry outEntry = null;
|
||||||
|
if (inEntry.getMethod() == JarEntry.STORED) continue;
|
||||||
|
// Create a new entry so that the compressed len is recomputed.
|
||||||
|
outEntry = new JarEntry(name);
|
||||||
outEntry.setTime(timestamp);
|
outEntry.setTime(timestamp);
|
||||||
out.putNextEntry(outEntry);
|
out.putNextEntry(outEntry);
|
||||||
in.getStream(name).writeTo(out);
|
InputStream data = in.getInputStream(inEntry);
|
||||||
|
while ((num = data.read(buffer)) > 0) {
|
||||||
|
out.write(buffer, 0, num);
|
||||||
|
}
|
||||||
out.flush();
|
out.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class WholeFileSignerOutputStream extends FilterOutputStream {
|
// This class is to provide a file's content, but trimming out the last two bytes
|
||||||
private boolean closing = false;
|
// Used for signWholeFile
|
||||||
private ByteArrayOutputStream footer = new ByteArrayOutputStream();
|
private static class CMSProcessableFile implements CMSTypedData {
|
||||||
private OutputStream tee;
|
|
||||||
public WholeFileSignerOutputStream(OutputStream out, OutputStream tee) {
|
|
||||||
super(out);
|
|
||||||
this.tee = tee;
|
|
||||||
}
|
|
||||||
public void notifyClosing() {
|
|
||||||
closing = true;
|
|
||||||
}
|
|
||||||
public void finish() throws IOException {
|
|
||||||
closing = false;
|
|
||||||
byte[] data = footer.toByteArray();
|
|
||||||
if (data.length < 2)
|
|
||||||
throw new IOException("Less than two bytes written to footer");
|
|
||||||
write(data, 0, data.length - 2);
|
|
||||||
}
|
|
||||||
public byte[] getTail() {
|
|
||||||
return footer.toByteArray();
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b) throws IOException {
|
|
||||||
write(b, 0, b.length);
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) throws IOException {
|
|
||||||
if (closing) {
|
|
||||||
// if the jar is about to close, save the footer that will be written
|
|
||||||
footer.write(b, off, len);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// write to both output streams. out is the CMSTypedData signer and tee is the file.
|
|
||||||
out.write(b, off, len);
|
|
||||||
tee.write(b, off, len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void write(int b) throws IOException {
|
|
||||||
if (closing) {
|
|
||||||
// if the jar is about to close, save the footer that will be written
|
|
||||||
footer.write(b);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// write to both output streams. out is the CMSTypedData signer and tee is the file.
|
|
||||||
out.write(b);
|
|
||||||
tee.write(b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CMSSigner implements CMSTypedData {
|
private File file;
|
||||||
private JarMap inputJar;
|
private ASN1ObjectIdentifier type;
|
||||||
private InputStream publicKeyFile;
|
private byte[] buffer;
|
||||||
private X509Certificate publicKey;
|
int bufferSize = 0;
|
||||||
private PrivateKey privateKey;
|
|
||||||
private OutputStream outputStream;
|
CMSProcessableFile(File file) {
|
||||||
private final ASN1ObjectIdentifier type;
|
this.file = file;
|
||||||
private WholeFileSignerOutputStream signer;
|
type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
|
||||||
public CMSSigner(JarMap inputJar, InputStream publicKeyFile,
|
buffer = new byte[4096];
|
||||||
X509Certificate publicKey, PrivateKey privateKey,
|
|
||||||
OutputStream outputStream) {
|
|
||||||
this.inputJar = inputJar;
|
|
||||||
this.publicKeyFile = publicKeyFile;
|
|
||||||
this.publicKey = publicKey;
|
|
||||||
this.privateKey = privateKey;
|
|
||||||
this.outputStream = outputStream;
|
|
||||||
this.type = new ASN1ObjectIdentifier(CMSObjectIdentifiers.data.getId());
|
|
||||||
}
|
|
||||||
public Object getContent() {
|
|
||||||
// Not supported, but still don't crash or return null
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public ASN1ObjectIdentifier getContentType() {
|
public ASN1ObjectIdentifier getContentType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
public void write(OutputStream out) throws IOException {
|
|
||||||
try {
|
@Override
|
||||||
signer = new WholeFileSignerOutputStream(out, outputStream);
|
public void write(OutputStream out) throws IOException, CMSException {
|
||||||
JarOutputStream outputJar = new JarOutputStream(signer);
|
FileInputStream input = new FileInputStream(file);
|
||||||
int hash = getDigestAlgorithm(publicKey);
|
long len = file.length() - 2;
|
||||||
// Assume the certificate is valid for at least an hour.
|
while ((bufferSize = input.read(buffer)) > 0) {
|
||||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
if (len <= bufferSize) {
|
||||||
Manifest manifest = addDigestsToManifest(inputJar, hash);
|
out.write(buffer, 0, (int) len);
|
||||||
copyFiles(manifest, inputJar, outputJar, timestamp);
|
break;
|
||||||
// Don't add Otacert, it's not an OTA
|
} else {
|
||||||
// addOtacert(outputJar, publicKeyFile, timestamp, manifest, hash);
|
out.write(buffer, 0, bufferSize);
|
||||||
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
|
}
|
||||||
signer.notifyClosing();
|
len -= bufferSize;
|
||||||
outputJar.close();
|
|
||||||
signer.finish();
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public void writeSignatureBlock(ByteArrayOutputStream temp)
|
|
||||||
throws IOException,
|
@Override
|
||||||
CertificateEncodingException,
|
public Object getContent() {
|
||||||
OperatorCreationException,
|
return file;
|
||||||
CMSException {
|
|
||||||
ZipUtils.writeSignatureBlock(this, publicKey, privateKey, temp);
|
|
||||||
}
|
}
|
||||||
public WholeFileSignerOutputStream getSigner() {
|
|
||||||
return signer;
|
byte[] getTail() {
|
||||||
|
return Arrays.copyOfRange(buffer, 0, bufferSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void signWholeFile(JarMap inputJar, InputStream publicKeyFile,
|
private static void signWholeFile(File input, X509Certificate publicKey,
|
||||||
X509Certificate publicKey, PrivateKey privateKey,
|
PrivateKey privateKey, OutputStream outputStream)
|
||||||
OutputStream outputStream) throws Exception {
|
throws Exception {
|
||||||
CMSSigner cmsOut = new CMSSigner(inputJar, publicKeyFile,
|
|
||||||
publicKey, privateKey, outputStream);
|
|
||||||
ByteArrayOutputStream temp = new ByteArrayOutputStream();
|
ByteArrayOutputStream temp = new ByteArrayOutputStream();
|
||||||
// put a readable message and a null char at the start of the
|
// put a readable message and a null char at the start of the
|
||||||
// archive comment, so that tools that display the comment
|
// archive comment, so that tools that display the comment
|
||||||
@@ -671,11 +548,14 @@ public class ZipUtils {
|
|||||||
byte[] message = "signed by SignApk".getBytes("UTF-8");
|
byte[] message = "signed by SignApk".getBytes("UTF-8");
|
||||||
temp.write(message);
|
temp.write(message);
|
||||||
temp.write(0);
|
temp.write(0);
|
||||||
cmsOut.writeSignatureBlock(temp);
|
|
||||||
byte[] zipData = cmsOut.getSigner().getTail();
|
CMSProcessableFile cmsFile = new CMSProcessableFile(input);
|
||||||
|
writeSignatureBlock(cmsFile, publicKey, privateKey, temp);
|
||||||
|
|
||||||
// For a zip with no archive comment, the
|
// For a zip with no archive comment, the
|
||||||
// end-of-central-directory record will be 22 bytes long, so
|
// end-of-central-directory record will be 22 bytes long, so
|
||||||
// we expect to find the EOCD marker 22 bytes from the end.
|
// we expect to find the EOCD marker 22 bytes from the end.
|
||||||
|
byte[] zipData = cmsFile.getTail();
|
||||||
if (zipData[zipData.length-22] != 0x50 ||
|
if (zipData[zipData.length-22] != 0x50 ||
|
||||||
zipData[zipData.length-21] != 0x4b ||
|
zipData[zipData.length-21] != 0x4b ||
|
||||||
zipData[zipData.length-20] != 0x05 ||
|
zipData[zipData.length-20] != 0x05 ||
|
||||||
@@ -713,12 +593,12 @@ public class ZipUtils {
|
|||||||
throw new IllegalArgumentException("found spurious EOCD header at " + i);
|
throw new IllegalArgumentException("found spurious EOCD header at " + i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
cmsFile.write(outputStream);
|
||||||
outputStream.write(total_size & 0xff);
|
outputStream.write(total_size & 0xff);
|
||||||
outputStream.write((total_size >> 8) & 0xff);
|
outputStream.write((total_size >> 8) & 0xff);
|
||||||
temp.writeTo(outputStream);
|
temp.writeTo(outputStream);
|
||||||
}
|
}
|
||||||
|
private static void signFile(Manifest manifest, JarFile inputJar,
|
||||||
private static void signFile(Manifest manifest, JarMap inputJar,
|
|
||||||
X509Certificate publicKey, PrivateKey privateKey,
|
X509Certificate publicKey, PrivateKey privateKey,
|
||||||
JarOutputStream outputJar)
|
JarOutputStream outputJar)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
@@ -729,7 +609,6 @@ public class ZipUtils {
|
|||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
manifest.write(outputJar);
|
manifest.write(outputJar);
|
||||||
// CERT.SF / CERT#.SF
|
|
||||||
je = new JarEntry(CERT_SF_NAME);
|
je = new JarEntry(CERT_SF_NAME);
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
@@ -737,11 +616,12 @@ public class ZipUtils {
|
|||||||
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
|
writeSignatureFile(manifest, baos, getDigestAlgorithm(publicKey));
|
||||||
byte[] signedData = baos.toByteArray();
|
byte[] signedData = baos.toByteArray();
|
||||||
outputJar.write(signedData);
|
outputJar.write(signedData);
|
||||||
// CERT.{DSA,EC,RSA} / CERT#.{DSA,EC,RSA}
|
// CERT.{EC,RSA} / CERT#.{EC,RSA}
|
||||||
je = new JarEntry((String.format(CERT_SIG_NAME, privateKey.getAlgorithm())));
|
final String keyType = publicKey.getPublicKey().getAlgorithm();
|
||||||
|
je = new JarEntry(String.format(CERT_SIG_NAME, keyType));
|
||||||
je.setTime(timestamp);
|
je.setTime(timestamp);
|
||||||
outputJar.putNextEntry(je);
|
outputJar.putNextEntry(je);
|
||||||
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
writeSignatureBlock(new CMSProcessableByteArray(signedData),
|
||||||
publicKey, privateKey, outputJar);
|
publicKey, privateKey, outputJar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
7
app/src/main/jni/CMakeLists.txt
Normal file
7
app/src/main/jni/CMakeLists.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.6)
|
||||||
|
add_library(zipadjust SHARED
|
||||||
|
jni_glue.c
|
||||||
|
zipadjust.c)
|
||||||
|
find_library(libz z)
|
||||||
|
find_library(liblog log)
|
||||||
|
target_link_libraries(zipadjust ${libz} ${liblog})
|
||||||
19
app/src/main/jni/jni_glue.c
Normal file
19
app/src/main/jni/jni_glue.c
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
//
|
||||||
|
// Java entry point
|
||||||
|
//
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
#include "zipadjust.h"
|
||||||
|
|
||||||
|
JNIEXPORT void JNICALL
|
||||||
|
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust(JNIEnv *env, jclass type, jstring filenameIn_,
|
||||||
|
jstring filenameOut_) {
|
||||||
|
const char *filenameIn = (*env)->GetStringUTFChars(env, filenameIn_, 0);
|
||||||
|
const char *filenameOut = (*env)->GetStringUTFChars(env, filenameOut_, 0);
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
zipadjust(filenameIn, filenameOut, 0);
|
||||||
|
|
||||||
|
(*env)->ReleaseStringUTFChars(env, filenameIn_, filenameIn);
|
||||||
|
(*env)->ReleaseStringUTFChars(env, filenameOut_, filenameOut);
|
||||||
|
}
|
||||||
@@ -1,35 +1,14 @@
|
|||||||
#include <jni.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <fcntl.h>
|
||||||
|
#include <string.h>
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
#include <android/log.h>
|
#include <unistd.h>
|
||||||
|
#include "zipadjust.h"
|
||||||
|
|
||||||
#define LOG_TAG "zipadjust"
|
#ifndef O_BINARY
|
||||||
|
#define O_BINARY 0
|
||||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
#define O_TEXT 0
|
||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
#endif
|
||||||
|
|
||||||
size_t insize, outsize = 0, alloc = 0;
|
|
||||||
unsigned char *fin, *fout;
|
|
||||||
|
|
||||||
int zipadjust(int decompress) ;
|
|
||||||
|
|
||||||
JNIEXPORT jbyteArray JNICALL
|
|
||||||
Java_com_topjohnwu_magisk_utils_ZipUtils_zipAdjust(JNIEnv *env, jclass type, jbyteArray jbytes,
|
|
||||||
jint size) {
|
|
||||||
fin = (*env)->GetPrimitiveArrayCritical(env, jbytes, NULL);
|
|
||||||
insize = (size_t) size;
|
|
||||||
|
|
||||||
zipadjust(0);
|
|
||||||
|
|
||||||
(*env)->ReleasePrimitiveArrayCritical(env, jbytes, fin, 0);
|
|
||||||
|
|
||||||
jbyteArray ret = (*env)->NewByteArray(env, outsize);
|
|
||||||
(*env)->SetByteArrayRegion(env, ret, 0, outsize, (const jbyte*) fout);
|
|
||||||
free(fout);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pragma pack(1)
|
#pragma pack(1)
|
||||||
struct local_header_struct {
|
struct local_header_struct {
|
||||||
@@ -107,41 +86,43 @@ static int xerror(char* message) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int xseekread(off_t offset, void* buf, size_t bytes) {
|
static int xseekread(int fd, off_t offset, void* buf, size_t bytes) {
|
||||||
memcpy(buf, fin + offset, bytes);
|
if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return xerror("Seek failed");
|
||||||
|
if (read(fd, buf, bytes) != bytes) return xerror("Read failed");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int xseekwrite(off_t offset, const void* buf, size_t bytes) {
|
static int xseekwrite(int fd, off_t offset, void* buf, size_t bytes) {
|
||||||
if (offset + bytes > outsize) outsize = offset + bytes;
|
if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return xerror("Seek failed");
|
||||||
if (outsize > alloc) {
|
if (write(fd, buf, bytes) != bytes) return xerror("Write failed");
|
||||||
fout = realloc(fout, outsize);
|
|
||||||
alloc = outsize;
|
|
||||||
}
|
|
||||||
memcpy(fout + offset, buf, bytes);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int xfilecopy(off_t offsetIn, off_t offsetOut, size_t bytes) {
|
static int xfilecopy(int fdIn, int fdOut, off_t offsetIn, off_t offsetOut, size_t bytes) {
|
||||||
unsigned int CHUNK = 256 * 1024;
|
if ((offsetIn != (off_t)-1) && (lseek(fdIn, offsetIn, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
|
||||||
unsigned char* buf = malloc(CHUNK);
|
if ((offsetOut != (off_t)-1) && (lseek(fdOut, offsetOut, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
|
||||||
|
|
||||||
|
int CHUNK = 256 * 1024;
|
||||||
|
void* buf = malloc(CHUNK);
|
||||||
if (buf == NULL) return xerror("malloc failed");
|
if (buf == NULL) return xerror("malloc failed");
|
||||||
size_t left = bytes;
|
size_t left = bytes;
|
||||||
while (left > 0) {
|
while (left > 0) {
|
||||||
size_t wanted = (left < CHUNK) ? left : CHUNK;
|
size_t wanted = (left < CHUNK) ? left : CHUNK;
|
||||||
xseekread(offsetIn, buf, wanted);
|
size_t r = read(fdIn, buf, wanted);
|
||||||
xseekwrite(offsetOut, buf, wanted);
|
if (r <= 0) return xerror("Read failed");
|
||||||
offsetIn += wanted;
|
if (write(fdOut, buf, r) != r) return xerror("Write failed");
|
||||||
offsetOut += wanted;
|
left -= r;
|
||||||
left -= wanted;
|
|
||||||
}
|
}
|
||||||
free(buf);
|
free(buf);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
|
static int xdecompress(int fdIn, int fdOut, off_t offsetIn, off_t offsetOut, size_t bytes) {
|
||||||
unsigned int CHUNK = 256 * 1024;
|
if ((offsetIn != (off_t)-1) && (lseek(fdIn, offsetIn, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
|
||||||
|
if ((offsetOut != (off_t)-1) && (lseek(fdOut, offsetOut, SEEK_SET) == (off_t)-1)) return xerror("Seek failed");
|
||||||
|
|
||||||
|
int CHUNK = 256 * 1024;
|
||||||
|
|
||||||
int ret;
|
int ret;
|
||||||
unsigned have;
|
unsigned have;
|
||||||
@@ -158,12 +139,9 @@ static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
|
|||||||
if (ret != Z_OK) return xerror("ret != Z_OK");
|
if (ret != Z_OK) return xerror("ret != Z_OK");
|
||||||
|
|
||||||
do {
|
do {
|
||||||
strm.avail_in = insize - offsetIn;
|
strm.avail_in = read(fdIn, in, CHUNK);
|
||||||
if (strm.avail_in == 0) break;
|
if (strm.avail_in == 0) break;
|
||||||
strm.avail_in = (strm.avail_in > CHUNK) ? CHUNK : strm.avail_in;
|
|
||||||
xseekread(offsetIn, in, strm.avail_in);
|
|
||||||
strm.next_in = in;
|
strm.next_in = in;
|
||||||
offsetIn += strm.avail_in;
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
strm.avail_out = CHUNK;
|
strm.avail_out = CHUNK;
|
||||||
@@ -181,8 +159,10 @@ static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
have = CHUNK - strm.avail_out;
|
have = CHUNK - strm.avail_out;
|
||||||
xseekwrite(offsetOut, out, have);
|
if (write(fdOut, out, have) != have) {
|
||||||
offsetOut += have;
|
(void)inflateEnd(&strm);
|
||||||
|
return xerror("Write failed");
|
||||||
|
}
|
||||||
} while (strm.avail_out == 0);
|
} while (strm.avail_out == 0);
|
||||||
} while (ret != Z_STREAM_END);
|
} while (ret != Z_STREAM_END);
|
||||||
(void)inflateEnd(&strm);
|
(void)inflateEnd(&strm);
|
||||||
@@ -190,118 +170,128 @@ static int xdecompress(off_t offsetIn, off_t offsetOut, size_t bytes) {
|
|||||||
return ret == Z_STREAM_END ? 1 : 0;
|
return ret == Z_STREAM_END ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int zipadjust(int decompress) {
|
int zipadjust(const char* filenameIn, const char* filenameOut, int decompress) {
|
||||||
int ok = 0;
|
int ok = 0;
|
||||||
|
|
||||||
char filename[1024];
|
int fin = open(filenameIn, O_RDONLY | O_BINARY);
|
||||||
|
if (fin > 0) {
|
||||||
|
unsigned int size = lseek(fin, 0, SEEK_END);
|
||||||
|
lseek(fin, 0, SEEK_SET);
|
||||||
|
LOGD("%d bytes\n", size);
|
||||||
|
|
||||||
central_footer_t central_footer;
|
char filename[1024];
|
||||||
uint32_t central_directory_in_position = 0;
|
|
||||||
uint32_t central_directory_in_size = 0;
|
|
||||||
uint32_t central_directory_out_size = 0;
|
|
||||||
|
|
||||||
int i;
|
central_footer_t central_footer;
|
||||||
for (i = insize - 4; i >= 0; i--) {
|
uint32_t central_directory_in_position = 0;
|
||||||
uint32_t magic = 0;
|
uint32_t central_directory_in_size = 0;
|
||||||
if (!xseekread(i, &magic, sizeof(uint32_t))) return 0;
|
uint32_t central_directory_out_size = 0;
|
||||||
if (magic == MAGIC_CENTRAL_FOOTER) {
|
|
||||||
LOGD("central footer @ %08X\n", i);
|
|
||||||
if (!xseekread(i, ¢ral_footer, sizeof(central_footer_t))) return 0;
|
|
||||||
|
|
||||||
central_header_t central_header;
|
int i;
|
||||||
if (!xseekread(central_footer.central_directory_offset, ¢ral_header, sizeof(central_header_t))) return 0;
|
for (i = size - 4; i >= 0; i--) {
|
||||||
if ( central_header.signature == MAGIC_CENTRAL_HEADER ) {
|
uint32_t magic = 0;
|
||||||
central_directory_in_position = central_footer.central_directory_offset;
|
if (!xseekread(fin, i, &magic, sizeof(uint32_t))) return 0;
|
||||||
central_directory_in_size = insize - central_footer.central_directory_offset;
|
if (magic == MAGIC_CENTRAL_FOOTER) {
|
||||||
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
|
LOGD("central footer @ %08X\n", i);
|
||||||
break;
|
if (!xseekread(fin, i, ¢ral_footer, sizeof(central_footer_t))) return 0;
|
||||||
|
|
||||||
|
central_header_t central_header;
|
||||||
|
if (!xseekread(fin, central_footer.central_directory_offset, ¢ral_header, sizeof(central_header_t))) return 0;
|
||||||
|
if ( central_header.signature == MAGIC_CENTRAL_HEADER ) {
|
||||||
|
central_directory_in_position = central_footer.central_directory_offset;
|
||||||
|
central_directory_in_size = size - central_footer.central_directory_offset;
|
||||||
|
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (central_directory_in_position == 0) return 0;
|
||||||
|
|
||||||
|
unsigned char* central_directory_in = (unsigned char*)malloc(central_directory_in_size);
|
||||||
|
unsigned char* central_directory_out = (unsigned char*)malloc(central_directory_in_size);
|
||||||
|
if (!xseekread(fin, central_directory_in_position, central_directory_in, central_directory_in_size)) return 0;
|
||||||
|
memset(central_directory_out, 0, central_directory_in_size);
|
||||||
|
|
||||||
|
unlink(filenameOut);
|
||||||
|
int fout = open(filenameOut, O_CREAT | O_WRONLY | O_BINARY, 0644);
|
||||||
|
if (fout > 0) {
|
||||||
|
|
||||||
|
uintptr_t central_directory_in_index = 0;
|
||||||
|
uintptr_t central_directory_out_index = 0;
|
||||||
|
|
||||||
|
central_header_t* central_header = NULL;
|
||||||
|
|
||||||
|
uint32_t out_index = 0;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
central_header = (central_header_t*)¢ral_directory_in[central_directory_in_index];
|
||||||
|
if (central_header->signature != MAGIC_CENTRAL_HEADER) break;
|
||||||
|
|
||||||
|
filename[central_header->length_filename] = (char)0;
|
||||||
|
memcpy(filename, ¢ral_directory_in[central_directory_in_index + sizeof(central_header_t)], central_header->length_filename);
|
||||||
|
LOGD("%s (%d --> %d) [%08X] (%d)\n", filename, central_header->size_uncompressed, central_header->size_compressed, central_header->crc32, central_header->length_extra + central_header->length_comment);
|
||||||
|
|
||||||
|
local_header_t local_header;
|
||||||
|
if (!xseekread(fin, central_header->offset, &local_header, sizeof(local_header_t))) return 0;
|
||||||
|
|
||||||
|
// save and update to next index before we clobber the data
|
||||||
|
uint16_t compression_method_old = central_header->compression_method;
|
||||||
|
uint32_t size_compressed_old = central_header->size_compressed;
|
||||||
|
uint32_t offset_old = central_header->offset;
|
||||||
|
uint32_t length_extra_old = central_header->length_extra;
|
||||||
|
central_directory_in_index += sizeof(central_header_t) + central_header->length_filename + central_header->length_extra + central_header->length_comment;
|
||||||
|
|
||||||
|
// copying, rewriting, and correcting local and central headers so all the information matches, and no data descriptors are necessary
|
||||||
|
central_header->offset = out_index;
|
||||||
|
central_header->flags = central_header->flags & !8;
|
||||||
|
if (decompress && (compression_method_old == 8)) {
|
||||||
|
central_header->compression_method = 0;
|
||||||
|
central_header->size_compressed = central_header->size_uncompressed;
|
||||||
|
}
|
||||||
|
central_header->length_extra = 0;
|
||||||
|
central_header->length_comment = 0;
|
||||||
|
local_header.compression_method = central_header->compression_method;
|
||||||
|
local_header.flags = central_header->flags;
|
||||||
|
local_header.crc32 = central_header->crc32;
|
||||||
|
local_header.size_uncompressed = central_header->size_uncompressed;
|
||||||
|
local_header.size_compressed = central_header->size_compressed;
|
||||||
|
local_header.length_extra = 0;
|
||||||
|
|
||||||
|
if (!xseekwrite(fout, out_index, &local_header, sizeof(local_header_t))) return 0;
|
||||||
|
out_index += sizeof(local_header_t);
|
||||||
|
if (!xseekwrite(fout, out_index, &filename[0], central_header->length_filename)) return 0;
|
||||||
|
out_index += central_header->length_filename;
|
||||||
|
|
||||||
|
if (decompress && (compression_method_old == 8)) {
|
||||||
|
if (!xdecompress(fin, fout, offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
|
||||||
|
} else {
|
||||||
|
if (!xfilecopy(fin, fout, offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
|
||||||
|
}
|
||||||
|
out_index += local_header.size_compressed;
|
||||||
|
|
||||||
|
memcpy(¢ral_directory_out[central_directory_out_index], central_header, sizeof(central_header_t) + central_header->length_filename);
|
||||||
|
central_directory_out_index += sizeof(central_header_t) + central_header->length_filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
central_directory_out_size = central_directory_out_index;
|
||||||
|
central_footer.central_directory_size = central_directory_out_size;
|
||||||
|
central_footer.central_directory_offset = out_index;
|
||||||
|
central_footer.length_comment = 0;
|
||||||
|
if (!xseekwrite(fout, out_index, central_directory_out, central_directory_out_size)) return 0;
|
||||||
|
out_index += central_directory_out_size;
|
||||||
|
if (!xseekwrite(fout, out_index, ¢ral_footer, sizeof(central_footer_t))) return 0;
|
||||||
|
|
||||||
|
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
|
||||||
|
LOGD("central footer @ %08X\n", out_index);
|
||||||
|
|
||||||
|
close(fout);
|
||||||
|
ok = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(central_directory_in);
|
||||||
|
free(central_directory_out);
|
||||||
|
close(fin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (central_directory_in_position == 0) return 0;
|
|
||||||
|
|
||||||
unsigned char* central_directory_in = (unsigned char*)malloc(central_directory_in_size);
|
|
||||||
unsigned char* central_directory_out = (unsigned char*)malloc(central_directory_in_size);
|
|
||||||
if (!xseekread(central_directory_in_position, central_directory_in, central_directory_in_size)) return 0;
|
|
||||||
memset(central_directory_out, 0, central_directory_in_size);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
fout = (unsigned char*) malloc(insize);
|
|
||||||
alloc = insize;
|
|
||||||
|
|
||||||
uintptr_t central_directory_in_index = 0;
|
|
||||||
uintptr_t central_directory_out_index = 0;
|
|
||||||
|
|
||||||
central_header_t* central_header = NULL;
|
|
||||||
|
|
||||||
uint32_t out_index = 0;
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
central_header = (central_header_t*)¢ral_directory_in[central_directory_in_index];
|
|
||||||
if (central_header->signature != MAGIC_CENTRAL_HEADER) break;
|
|
||||||
|
|
||||||
filename[central_header->length_filename] = (char)0;
|
|
||||||
memcpy(filename, ¢ral_directory_in[central_directory_in_index + sizeof(central_header_t)], central_header->length_filename);
|
|
||||||
LOGD("%s (%d --> %d) [%08X] (%d)\n", filename, central_header->size_uncompressed, central_header->size_compressed, central_header->crc32, central_header->length_extra + central_header->length_comment);
|
|
||||||
|
|
||||||
local_header_t local_header;
|
|
||||||
if (!xseekread(central_header->offset, &local_header, sizeof(local_header_t))) return 0;
|
|
||||||
|
|
||||||
// save and update to next index before we clobber the data
|
|
||||||
uint16_t compression_method_old = central_header->compression_method;
|
|
||||||
uint32_t size_compressed_old = central_header->size_compressed;
|
|
||||||
uint32_t offset_old = central_header->offset;
|
|
||||||
uint32_t length_extra_old = central_header->length_extra;
|
|
||||||
central_directory_in_index += sizeof(central_header_t) + central_header->length_filename + central_header->length_extra + central_header->length_comment;
|
|
||||||
|
|
||||||
// copying, rewriting, and correcting local and central headers so all the information matches, and no data descriptors are necessary
|
|
||||||
central_header->offset = out_index;
|
|
||||||
central_header->flags = central_header->flags & !8;
|
|
||||||
if (decompress && (compression_method_old == 8)) {
|
|
||||||
central_header->compression_method = 0;
|
|
||||||
central_header->size_compressed = central_header->size_uncompressed;
|
|
||||||
}
|
|
||||||
central_header->length_extra = 0;
|
|
||||||
central_header->length_comment = 0;
|
|
||||||
local_header.compression_method = central_header->compression_method;
|
|
||||||
local_header.flags = central_header->flags;
|
|
||||||
local_header.crc32 = central_header->crc32;
|
|
||||||
local_header.size_uncompressed = central_header->size_uncompressed;
|
|
||||||
local_header.size_compressed = central_header->size_compressed;
|
|
||||||
local_header.length_extra = 0;
|
|
||||||
|
|
||||||
if (!xseekwrite(out_index, &local_header, sizeof(local_header_t))) return 0;
|
|
||||||
out_index += sizeof(local_header_t);
|
|
||||||
if (!xseekwrite(out_index, &filename[0], central_header->length_filename)) return 0;
|
|
||||||
out_index += central_header->length_filename;
|
|
||||||
|
|
||||||
if (decompress && (compression_method_old == 8)) {
|
|
||||||
if (!xdecompress(offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
|
|
||||||
} else {
|
|
||||||
if (!xfilecopy(offset_old + sizeof(local_header_t) + central_header->length_filename + length_extra_old, out_index, size_compressed_old)) return 0;
|
|
||||||
}
|
|
||||||
out_index += local_header.size_compressed;
|
|
||||||
|
|
||||||
memcpy(¢ral_directory_out[central_directory_out_index], central_header, sizeof(central_header_t) + central_header->length_filename);
|
|
||||||
central_directory_out_index += sizeof(central_header_t) + central_header->length_filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
central_directory_out_size = central_directory_out_index;
|
|
||||||
central_footer.central_directory_size = central_directory_out_size;
|
|
||||||
central_footer.central_directory_offset = out_index;
|
|
||||||
central_footer.length_comment = 0;
|
|
||||||
if (!xseekwrite(out_index, central_directory_out, central_directory_out_size)) return 0;
|
|
||||||
out_index += central_directory_out_size;
|
|
||||||
if (!xseekwrite(out_index, ¢ral_footer, sizeof(central_footer_t))) return 0;
|
|
||||||
|
|
||||||
LOGD("central header @ %08X (%d)\n", central_footer.central_directory_offset, central_footer.central_directory_size);
|
|
||||||
LOGD("central footer @ %08X\n", out_index);
|
|
||||||
|
|
||||||
ok = 1;
|
|
||||||
|
|
||||||
free(central_directory_in);
|
|
||||||
free(central_directory_out);
|
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|||||||
13
app/src/main/jni/zipadjust.h
Normal file
13
app/src/main/jni/zipadjust.h
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef MAGISKMANAGER_ZIPADJUST_H_H
|
||||||
|
#define MAGISKMANAGER_ZIPADJUST_H_H
|
||||||
|
|
||||||
|
#include <android/log.h>
|
||||||
|
|
||||||
|
int zipadjust(const char* filenameIn, const char* filenameOut, int decompress);
|
||||||
|
|
||||||
|
#define LOG_TAG "zipadjust"
|
||||||
|
|
||||||
|
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
#endif //MAGISKMANAGER_ZIPADJUST_H_H
|
||||||
BIN
app/src/main/jniLibs/armeabi-v7a/libbusybox.so
Normal file
BIN
app/src/main/jniLibs/armeabi-v7a/libbusybox.so
Normal file
Binary file not shown.
BIN
app/src/main/jniLibs/x86/libbusybox.so
Normal file
BIN
app/src/main/jniLibs/x86/libbusybox.so
Normal file
Binary file not shown.
13
app/src/main/res/drawable/ic_arrow.xml
Normal file
13
app/src/main/res/drawable/ic_arrow.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:width="24dp">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
|
||||||
|
<path
|
||||||
|
android:pathData="M0-.75h24v24H0z" />
|
||||||
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24.0"
|
|
||||||
android:viewportWidth="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#000"
|
|
||||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -4,6 +4,6 @@
|
|||||||
android:viewportHeight="24.0"
|
android:viewportHeight="24.0"
|
||||||
android:viewportWidth="24.0">
|
android:viewportWidth="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#757575"
|
android:fillColor="#000"
|
||||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24.0"
|
|
||||||
android:viewportWidth="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#000"
|
|
||||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
|
||||||
</vector>
|
|
||||||
9
app/src/main/res/drawable/ic_more.xml
Normal file
9
app/src/main/res/drawable/ic_more.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M6,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM18,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_notifications.xml
Normal file
9
app/src/main/res/drawable/ic_notifications.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M7.58,4.08L6.15,2.65C3.75,4.48 2.17,7.3 2.03,10.5h2c0.15,-2.65 1.51,-4.97 3.55,-6.42zM19.97,10.5h2c-0.15,-3.2 -1.73,-6.02 -4.12,-7.85l-1.42,1.43c2.02,1.45 3.39,3.77 3.54,6.42zM18,11c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2v-5zM12,22c0.14,0 0.27,-0.01 0.4,-0.04 0.65,-0.14 1.18,-0.58 1.44,-1.18 0.1,-0.24 0.15,-0.5 0.15,-0.78h-4c0.01,1.1 0.9,2 2.01,2z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_safetynet.xml
Normal file
9
app/src/main/res/drawable/ic_safetynet.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#fff"
|
||||||
|
android:pathData="M10,16h4c0.55,0 1,-0.45 1,-1v-3c0,-0.55 -0.45,-1 -1,-1v-1c0,-1.11 -0.9,-2 -2,-2 -1.11,0 -2,0.9 -2,2v1c-0.55,0 -1,0.45 -1,1v3c0,0.55 0.45,1 1,1zM10.8,10c0,-0.66 0.54,-1.2 1.2,-1.2 0.66,0 1.2,0.54 1.2,1.2v1h-2.4v-1zM17,1L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14z"/>
|
||||||
|
</vector>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportHeight="24.0"
|
|
||||||
android:viewportWidth="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
|
|
||||||
</vector>
|
|
||||||
9
app/src/main/res/drawable/ic_su_warning.xml
Normal file
9
app/src/main/res/drawable/ic_su_warning.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="18dp"
|
||||||
|
android:height="18dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFd32f2f"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||||
|
</vector>
|
||||||
7
app/src/main/res/drawable/ic_superuser.xml
Normal file
7
app/src/main/res/drawable/ic_superuser.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" />
|
||||||
|
</vector>
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
<include layout="@layout/toolbar"/>
|
<include layout="@layout/toolbar"/>
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
@@ -57,35 +56,35 @@
|
|||||||
android:textAppearance="@style/TextAppearance.AppCompat.Headline"/>
|
android:textAppearance="@style/TextAppearance.AppCompat.Headline"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/app_version_info"
|
android:id="@+id/app_version_info"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/ic_info_outline"
|
app:icon="@drawable/ic_info_outline"
|
||||||
app:text="@string/app_version"/>
|
app:text="@string/app_version"/>
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/app_changelog"
|
android:id="@+id/app_changelog"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/ic_history"
|
app:icon="@drawable/ic_history"
|
||||||
app:text="@string/app_changelog"/>
|
app:text="@string/app_changelog"/>
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/app_developers"
|
android:id="@+id/app_developers"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/ic_person"
|
app:icon="@drawable/ic_person"
|
||||||
app:text="@string/app_developers"/>
|
app:text="@string/app_developers"/>
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/app_translators"
|
android:id="@+id/app_translators"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/ic_language"
|
app:icon="@drawable/ic_language"
|
||||||
app:text="@string/app_translators"/>
|
app:text="@string/app_translators"/>
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/app_source_code"
|
android:id="@+id/app_source_code"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -110,14 +109,14 @@
|
|||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/support_thread"
|
android:id="@+id/support_thread"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:icon="@drawable/ic_xda"
|
app:icon="@drawable/ic_xda"
|
||||||
app:text="@string/support_thread"/>
|
app:text="@string/support_thread"/>
|
||||||
|
|
||||||
<com.topjohnwu.magisk.AboutCardRow
|
<com.topjohnwu.magisk.components.AboutCardRow
|
||||||
android:id="@+id/donation"
|
android:id="@+id/donation"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user