mirror of
https://github.com/topjohnwu/Magisk.git
synced 2025-08-14 15:27:25 +00:00
Compare commits
245 Commits
manager-v8
...
manager-v8
Author | SHA1 | Date | |
---|---|---|---|
![]() |
7f8257152f | ||
![]() |
0cd80f2556 | ||
![]() |
1717387876 | ||
![]() |
109363ebf6 | ||
![]() |
716c4fa386 | ||
![]() |
9a09b4eb20 | ||
![]() |
95a5b57265 | ||
![]() |
13fbf397d1 | ||
![]() |
20be99ec8a | ||
![]() |
04c53c3578 | ||
![]() |
51bc27a869 | ||
![]() |
71b083794c | ||
![]() |
b100d0c503 | ||
![]() |
76061296c9 | ||
![]() |
bb303d2da1 | ||
![]() |
c91c070343 | ||
![]() |
aec06a6f61 | ||
![]() |
e8ba671fc2 | ||
![]() |
1860e5d133 | ||
![]() |
f2cb3c38fe | ||
![]() |
9a28dd4f6e | ||
![]() |
d2acd59ea8 | ||
![]() |
79dfdb29e7 | ||
![]() |
eb21c8b42e | ||
![]() |
541bb53553 | ||
![]() |
fe8997efae | ||
![]() |
23455c722c | ||
![]() |
5ce29c30d2 | ||
![]() |
70d67728fd | ||
![]() |
e546884b08 | ||
![]() |
b36e6d987d | ||
![]() |
53c3dd5e8b | ||
![]() |
da723b207a | ||
![]() |
e050f77198 | ||
![]() |
540b4b7ea9 | ||
![]() |
bbef22daf7 | ||
![]() |
9ed110c91b | ||
![]() |
a30d510eb1 | ||
![]() |
ef98eaed8f | ||
![]() |
2a257f327c | ||
![]() |
4060c2107c | ||
![]() |
cd23d27048 | ||
![]() |
18b86e4fd2 | ||
![]() |
5f2e22a259 | ||
![]() |
4e97b18977 | ||
![]() |
f9bde347bc | ||
![]() |
947a7d6a2f | ||
![]() |
872ab2e99b | ||
![]() |
90b8813bb7 | ||
![]() |
88d0f63294 | ||
![]() |
79fa0d3a90 | ||
![]() |
8e61080a4a | ||
![]() |
3f9a64417b | ||
![]() |
eb959379e8 | ||
![]() |
41a644afb9 | ||
![]() |
6b42db943d | ||
![]() |
1c325459eb | ||
![]() |
6d88d8ad95 | ||
![]() |
246997f273 | ||
![]() |
b6144ae582 | ||
![]() |
afe17c73b4 | ||
![]() |
b51b884fc7 | ||
![]() |
d3e4b29e62 | ||
![]() |
24059e7403 | ||
![]() |
107a2a6682 | ||
![]() |
56b4ab6672 | ||
![]() |
4662454938 | ||
![]() |
db4f78d463 | ||
![]() |
880de21596 | ||
![]() |
622dd84c9e | ||
![]() |
f983bfc883 | ||
![]() |
45cdb3fdb0 | ||
![]() |
9a707236b8 | ||
![]() |
e9e6ad3bb0 | ||
![]() |
ab78a81d15 | ||
![]() |
18340099b7 | ||
![]() |
a013696a41 | ||
![]() |
8a2a6d9232 | ||
![]() |
12aa6d86e4 | ||
![]() |
7d08969d28 | ||
![]() |
dda4aa8488 | ||
![]() |
cdaef3d801 | ||
![]() |
9159166128 | ||
![]() |
dc0882e043 | ||
![]() |
c811f015ef | ||
![]() |
d8f0b66fe1 | ||
![]() |
dc3d57deba | ||
![]() |
d089698475 | ||
![]() |
8ed2dd6687 | ||
![]() |
50305ca1fe | ||
![]() |
3e91567636 | ||
![]() |
0b4dd63d36 | ||
![]() |
38d0f85deb | ||
![]() |
c5b452f369 | ||
![]() |
6ce9225f52 | ||
![]() |
13a8820603 | ||
![]() |
503997a09a | ||
![]() |
17efdff134 | ||
![]() |
984f32f994 | ||
![]() |
eee7f097e3 | ||
![]() |
086059ec30 | ||
![]() |
7ff22c68c7 | ||
![]() |
1232113772 | ||
![]() |
039d4936cb | ||
![]() |
784dd80965 | ||
![]() |
1ffe9bd83b | ||
![]() |
0c28b23224 | ||
![]() |
ec1af9dc1e | ||
![]() |
ff4cea229a | ||
![]() |
3f81f9371f | ||
![]() |
60e89a7d22 | ||
![]() |
c50daa5c9e | ||
![]() |
58d00ab863 | ||
![]() |
ce916459c5 | ||
![]() |
4094d560ab | ||
![]() |
4dbf7eb04b | ||
![]() |
a39577c44d | ||
![]() |
125ee46685 | ||
![]() |
ce84f1762c | ||
![]() |
a687d1347b | ||
![]() |
6d9db20614 | ||
![]() |
c62dfc1bcc | ||
![]() |
aabe2696fe | ||
![]() |
ae0d605310 | ||
![]() |
2a694596b5 | ||
![]() |
ff0a76606e | ||
![]() |
dead74801d | ||
![]() |
ab207a1bb3 | ||
![]() |
f152e8c33d | ||
![]() |
797ba4fbf4 | ||
![]() |
a848f10bba | ||
![]() |
552ec1eb35 | ||
![]() |
1385d2a4f4 | ||
![]() |
3b5c9abf7a | ||
![]() |
e0fa032bd3 | ||
![]() |
7b69650fcd | ||
![]() |
08a8df489f | ||
![]() |
9f35a8a520 | ||
![]() |
0df891b336 | ||
![]() |
385853a290 | ||
![]() |
fa3ef8a1c1 | ||
![]() |
c93ada03c7 | ||
![]() |
0064b01ae0 | ||
![]() |
1469b82aa2 | ||
![]() |
2d5cf8a6fe | ||
![]() |
290959f74c | ||
![]() |
4d9f58ee72 | ||
![]() |
9241246de6 | ||
![]() |
58a5d52b78 | ||
![]() |
2906178ac3 | ||
![]() |
e0afbb647b | ||
![]() |
50be50cf6a | ||
![]() |
77a9d3a5bc | ||
![]() |
f9c7a4c933 | ||
![]() |
2b759b84b0 | ||
![]() |
1e45c63ea5 | ||
![]() |
b14a260827 | ||
![]() |
ade1597e03 | ||
![]() |
2739d3cb67 | ||
![]() |
dc5e78e142 | ||
![]() |
e9759a5868 | ||
![]() |
e7ab802498 | ||
![]() |
42672c2e27 | ||
![]() |
e65d61d313 | ||
![]() |
076da5c7c4 | ||
![]() |
9deaf2507c | ||
![]() |
5c114c67de | ||
![]() |
d904cb0441 | ||
![]() |
bd1dd9d863 | ||
![]() |
afebe734b8 | ||
![]() |
e21a78164e | ||
![]() |
1e0f96d0fd | ||
![]() |
bf650332d8 | ||
![]() |
f32e0af830 | ||
![]() |
4c94f90e5d | ||
![]() |
ffb4224640 | ||
![]() |
89fff4830b | ||
![]() |
16e4c67992 | ||
![]() |
cf47214ee4 | ||
![]() |
0feab753fb | ||
![]() |
d0b6318b90 | ||
![]() |
966e23b846 | ||
![]() |
5b8a1fc2a7 | ||
![]() |
02ea3ca525 | ||
![]() |
0632b146b8 | ||
![]() |
1b0b180761 | ||
![]() |
0d11f73a1d | ||
![]() |
533cb8eb58 | ||
![]() |
8ac1181e9a | ||
![]() |
5ca1892eb0 | ||
![]() |
e32db6a0e8 | ||
![]() |
82fff615d6 | ||
![]() |
24a8f0808d | ||
![]() |
4a7c3c06bc | ||
![]() |
da93bbc1fe | ||
![]() |
fa2dbe981e | ||
![]() |
ce6cceae8b | ||
![]() |
7b26e8b818 | ||
![]() |
2da5fcb00b | ||
![]() |
a079966f97 | ||
![]() |
468796c23d | ||
![]() |
5833aadef5 | ||
![]() |
eb261c8026 | ||
![]() |
a4c48847d1 | ||
![]() |
43288be091 | ||
![]() |
1ad7a6fe93 | ||
![]() |
4e0a3f5e72 | ||
![]() |
d7c33f647d | ||
![]() |
9087207dc0 | ||
![]() |
2760f37e6b | ||
![]() |
3fa3426032 | ||
![]() |
2e4dc91b96 | ||
![]() |
aaaaa3d044 | ||
![]() |
1edc4449d5 | ||
![]() |
f3cd4da026 | ||
![]() |
872c55207c | ||
![]() |
339ca6d666 | ||
![]() |
4aeac3b8f4 | ||
![]() |
d625beb7f3 | ||
![]() |
735b65c50c | ||
![]() |
efb1eab327 | ||
![]() |
49d4785da0 | ||
![]() |
28e65ce383 | ||
![]() |
c3b6a48373 | ||
![]() |
a42ebd429b | ||
![]() |
8f89010752 | ||
![]() |
105a18f719 | ||
![]() |
eb04ca4c4a | ||
![]() |
6092d7ca88 | ||
![]() |
66cad101c0 | ||
![]() |
0a14f43f9c | ||
![]() |
311c1f0dfd | ||
![]() |
0499588107 | ||
![]() |
d4d837a562 | ||
![]() |
fbcbb20178 | ||
![]() |
0914700fc6 | ||
![]() |
eeced2fb5b | ||
![]() |
6509e3d4f5 | ||
![]() |
317052604b | ||
![]() |
5538f7168c | ||
![]() |
dcb9e4cd93 | ||
![]() |
d9382f59bf | ||
![]() |
403a0c770a | ||
![]() |
f0f1cdc501 | ||
![]() |
4e272b70ef |
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## READ BEFORE OPENING ISSUES
|
||||
|
||||
All bug reports require you to **USE CANARY BUILDS**. Please include the version name and version code in the bug report.
|
||||
|
||||
If you experience a bootloop, attach a `dmesg` (kernel logs) when the device refuse to boot. This may very likely require a custom kernel on some devices as `last_kmsg` or `pstore ramoops` are usually not enabled by default. In addition, please also upload the result of `cat /proc/mounts` when your device is working correctly **WITHOUT ROOT**.
|
||||
|
||||
If you experience issues during installation, in recovery, upload the recovery logs, or in Magisk Manager, upload the install logs. Please also upload the `boot.img` or `recovery.img` that you are using for patching.
|
||||
|
||||
If you experience a crash of Magisk Manager, dump the full `logcat` **when the crash happens**. **DO NOT** upload `magisk.log`.
|
||||
|
||||
If you experience other issues related to Magisk, upload `magisk.log`, and preferably also include a boot `logcat` (start dumping `logcat` when the device boots up)
|
||||
|
||||
**DO NOT** open issues regarding root detection.
|
||||
|
||||
**DO NOT** ask for instructions.
|
||||
|
||||
**DO NOT** report issues if you have any modules installed.
|
||||
|
||||
Without following the rules above, your issue will be closed without explanation.
|
94
.github/workflows/build.yml
vendored
Normal file
94
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
name: Magisk Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'native/**'
|
||||
- 'stub/**'
|
||||
- 'buildSrc/**'
|
||||
- 'build.py'
|
||||
- 'gradle.properties'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macOS-latest ]
|
||||
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: '11'
|
||||
|
||||
- name: Set up Python 3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Set up GitHub env (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
$oldAndroidPath = $env:ANDROID_SDK_ROOT
|
||||
$sdk_root = "C:\Android"
|
||||
New-Item -Path $sdk_root -ItemType SymbolicLink -Value $oldAndroidPath
|
||||
$ndk_ver = Select-String -Path "gradle.properties" -Pattern "^magisk.fullNdkVersion=" | % { $_ -replace ".*=" }
|
||||
echo "ANDROID_SDK_ROOT=$sdk_root" >> $env:GITHUB_ENV
|
||||
echo "ANDROID_HOME=$sdk_root" >> $env:GITHUB_ENV
|
||||
echo "MAGISK_NDK_VERSION=$ndk_ver" >> $env:GITHUB_ENV
|
||||
echo "GRADLE_OPTS=-Dorg.gradle.daemon=false" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Set up GitHub env (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
ndk_ver=$(sed -n 's/^magisk.fullNdkVersion=//p' gradle.properties)
|
||||
echo ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT >> $GITHUB_ENV
|
||||
echo MAGISK_NDK_VERSION=$ndk_ver >> $GITHUB_ENV
|
||||
|
||||
- name: Cache Gradle
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle.kts') }}
|
||||
restore-keys: ${{ runner.os }}-gradle-
|
||||
|
||||
- name: Cache NDK
|
||||
id: ndk-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ env.ANDROID_SDK_ROOT }}/ndk/magisk
|
||||
key: ${{ runner.os }}-ndk-${{ env.MAGISK_NDK_VERSION }}
|
||||
|
||||
- name: Set up NDK
|
||||
if: steps.ndk-cache.outputs.cache-hit != 'true'
|
||||
run: python build.py ndk
|
||||
|
||||
- name: Build release
|
||||
run: python build.py -vr all
|
||||
|
||||
- name: Build debug
|
||||
run: python build.py -v all
|
||||
|
||||
# Only upload artifacts built on Linux
|
||||
- name: Upload build artifact
|
||||
if: runner.os == 'Linux'
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ github.sha }}
|
||||
path: out
|
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -25,6 +25,9 @@
|
||||
[submodule "pcre"]
|
||||
path = native/jni/external/pcre
|
||||
url = https://android.googlesource.com/platform/external/pcre
|
||||
[submodule "xhook"]
|
||||
path = native/jni/external/xhook
|
||||
url = https://github.com/iqiyi/xHook.git
|
||||
[submodule "termux-elf-cleaner"]
|
||||
path = tools/termux-elf-cleaner
|
||||
url = https://github.com/termux/termux-elf-cleaner.git
|
||||
|
12
README.MD
12
README.MD
@@ -15,11 +15,11 @@ Here are some feature highlights:
|
||||
|
||||
## Downloads
|
||||
|
||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.1/MagiskManager-v8.0.1.apk)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.5/MagiskManager-v8.0.5.apk)
|
||||
[](https://raw.githubusercontent.com/topjohnwu/magisk_files/canary/app-debug.apk)
|
||||
<br>
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v20.4)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v21.0)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v21.2)
|
||||
[](https://github.com/topjohnwu/Magisk/releases/tag/v21.2)
|
||||
|
||||
## Useful Links
|
||||
|
||||
@@ -56,13 +56,13 @@ For Magisk Manager crashes, record and upload the logcat when the crash occurs.
|
||||
- macOS: `export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"`
|
||||
- Linux: `export PATH="/path/to/androidstudio/jre/bin:$PATH"`
|
||||
- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
|
||||
- Set environment variable `ANDROID_HOME` to the Android SDK folder (can be found in Android Studio settings)
|
||||
- Set environment variable `ANDROID_SDK_ROOT` to the Android SDK folder (can be found in Android Studio settings)
|
||||
- Run `./build.py ndk` to let the script download and install NDK for you
|
||||
- Set configurations in `config.prop`. A sample `config.prop.sample` is provided.
|
||||
- To start building, run `build.py` to see your options. \
|
||||
For each action, use `-h` to access help (e.g. `./build.py all -h`)
|
||||
- To start development, open the project in Android Studio. Both app (Kotlin/Java) and native (C++/C) source code can be properly developed using the IDE, but *always* use `build.py` for building.
|
||||
- `build.py` builds in debug mode by default. If you want release builds (with `-r, --release`), you need a Java Keystore to sign APKs and zips. For more information, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
||||
- Optionally, set custom configs with `config.prop`. A sample `config.prop.sample` is provided.
|
||||
- To sign APKs and zips with your own private keys, set signing configs in `config.prop`. For more info, check [Google's Documentation](https://developer.android.com/studio/publish/app-signing.html#generate-key).
|
||||
|
||||
## Translation Contributions
|
||||
|
||||
|
@@ -1,7 +1,9 @@
|
||||
import java.io.PrintStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
kotlin("android")
|
||||
kotlin("android.extensions")
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("kapt")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
}
|
||||
@@ -20,9 +22,8 @@ android {
|
||||
applicationId = "com.topjohnwu.magisk"
|
||||
vectorDrawables.useSupportLibrary = true
|
||||
multiDexEnabled = true
|
||||
versionName = Config["appVersion"]
|
||||
versionCode = Config["appVersionCode"]?.toInt()
|
||||
buildConfigField("int", "LATEST_MAGISK", Config["versionCode"] ?: "Integer.MAX_VALUE")
|
||||
versionName = Config.appVersion
|
||||
versionCode = Config.appVersionCode
|
||||
|
||||
javaCompileOptions.annotationProcessorOptions.arguments(
|
||||
mapOf("room.incremental" to "true")
|
||||
@@ -51,11 +52,12 @@ android {
|
||||
|
||||
packagingOptions {
|
||||
exclude("/META-INF/**")
|
||||
exclude("/androidsupportmultidexversion.txt")
|
||||
exclude("/org/bouncycastle/**")
|
||||
exclude("/kotlin/**")
|
||||
exclude("/kotlinx/**")
|
||||
exclude("/okhttp3/**")
|
||||
exclude("/*.txt")
|
||||
exclude("/*.bin")
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
@@ -63,28 +65,61 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
androidExtensions {
|
||||
isExperimental = true
|
||||
}
|
||||
|
||||
val copyUtils = tasks.register("copyUtils", Copy::class) {
|
||||
tasks["preBuild"]?.dependsOn(tasks.register("copyUtils", Copy::class) {
|
||||
from(rootProject.file("scripts/util_functions.sh"))
|
||||
into("src/main/res/raw")
|
||||
})
|
||||
|
||||
android.applicationVariants.all {
|
||||
val keysDir = rootProject.file("tools/keys")
|
||||
val outSrcDir = File(buildDir, "generated/source/keydata/$name")
|
||||
val outSrc = File(outSrcDir, "com/topjohnwu/signing/KeyData.java")
|
||||
|
||||
fun PrintStream.newField(name: String, file: File) {
|
||||
println("public static byte[] $name() {")
|
||||
print("byte[] buf = {")
|
||||
val bytes = file.readBytes()
|
||||
print(bytes.joinToString(",") { "(byte)(${it.toInt() and 0xff})" })
|
||||
println("};")
|
||||
println("return buf;")
|
||||
println("}")
|
||||
}
|
||||
|
||||
tasks["preBuild"]?.dependsOn(copyUtils)
|
||||
val genSrcTask = tasks.register("generate${name.capitalize()}KeyData") {
|
||||
inputs.dir(keysDir)
|
||||
outputs.file(outSrc)
|
||||
doLast {
|
||||
outSrc.parentFile.mkdirs()
|
||||
PrintStream(outSrc).use {
|
||||
it.println("package com.topjohnwu.signing;")
|
||||
it.println("public final class KeyData {")
|
||||
|
||||
it.newField("testCert", File(keysDir, "testkey.x509.pem"))
|
||||
it.newField("testKey", File(keysDir, "testkey.pk8"))
|
||||
it.newField("verityCert", File(keysDir, "verity.x509.pem"))
|
||||
it.newField("verityKey", File(keysDir, "verity.pk8"))
|
||||
|
||||
it.println("}")
|
||||
}
|
||||
}
|
||||
}
|
||||
registerJavaGeneratingTask(genSrcTask.get(), outSrcDir)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
implementation(kotlin("stdlib"))
|
||||
implementation(project(":app:shared"))
|
||||
implementation(project(":app:signing"))
|
||||
|
||||
implementation("com.github.topjohnwu:jtar:1.0.0")
|
||||
implementation("com.github.topjohnwu:indeterminate-checkbox:1.0.7")
|
||||
implementation("com.github.topjohnwu:lz4-java:1.7.1")
|
||||
implementation("com.jakewharton.timber:timber:4.7.1")
|
||||
|
||||
val vBC = "1.68"
|
||||
implementation("org.bouncycastle:bcprov-jdk15on:${vBC}")
|
||||
implementation("org.bouncycastle:bcpkix-jdk15on:${vBC}")
|
||||
|
||||
val vBAdapt = "4.0.0"
|
||||
val bindingAdapter = "me.tatarka.bindingcollectionadapter2:bindingcollectionadapter"
|
||||
implementation("${bindingAdapter}:${vBAdapt}")
|
||||
@@ -123,7 +158,7 @@ dependencies {
|
||||
implementation("com.squareup.moshi:moshi:${vMoshi}")
|
||||
kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
|
||||
|
||||
val vRoom = "2.2.5"
|
||||
val vRoom = "2.3.0-alpha04"
|
||||
implementation("androidx.room:room-runtime:${vRoom}")
|
||||
implementation("androidx.room:room-ktx:${vRoom}")
|
||||
kapt("androidx.room:room-compiler:${vRoom}")
|
||||
@@ -133,9 +168,9 @@ dependencies {
|
||||
implementation("androidx.navigation:navigation-ui-ktx:${vNav}")
|
||||
|
||||
implementation("androidx.biometric:biometric:1.0.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.0.1")
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.0.4")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("androidx.browser:browser:1.2.0")
|
||||
implementation("androidx.browser:browser:1.3.0")
|
||||
implementation("androidx.preference:preference:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||
implementation("androidx.fragment:fragment-ktx:1.2.5")
|
||||
|
6
app/proguard-rules.pro
vendored
6
app/proguard-rules.pro
vendored
@@ -35,12 +35,6 @@
|
||||
void onResponse(org.json.JSONObject);
|
||||
}
|
||||
|
||||
# Fragments
|
||||
# TODO: Remove when AGP 4.1 release
|
||||
# https://issuetracker.google.com/issues/142601969
|
||||
-keep,allowobfuscation class * extends androidx.fragment.app.Fragment
|
||||
-keepnames class androidx.navigation.fragment.NavHostFragment
|
||||
|
||||
# Strip Timber verbose and debug logging
|
||||
-assumenosideeffects class timber.log.Timber.Tree {
|
||||
public void v(**);
|
||||
|
@@ -12,12 +12,6 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
tools:ignore="UnusedAttribute">
|
||||
|
||||
<provider
|
||||
android:name="a.p"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@@ -8,7 +8,6 @@ import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
@@ -34,8 +33,6 @@ public class FileProvider extends ContentProvider {
|
||||
|
||||
private PathStrategy mStrategy;
|
||||
|
||||
public static ProviderCallHandler callHandler;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
@@ -131,13 +128,6 @@ public class FileProvider extends ContentProvider {
|
||||
return file.delete() ? 1 : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
if (callHandler != null)
|
||||
return callHandler.call(getContext(), method, arg, extras);
|
||||
return Bundle.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode)
|
||||
throws FileNotFoundException {
|
||||
|
@@ -1,8 +0,0 @@
|
||||
package com.topjohnwu.magisk;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
public interface ProviderCallHandler {
|
||||
Bundle call(Context context, String method, String arg, Bundle extras);
|
||||
}
|
1
app/signing/.gitignore
vendored
1
app/signing/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
@@ -1,35 +0,0 @@
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
plugins {
|
||||
id("java-library")
|
||||
id("java")
|
||||
id("com.github.johnrengelman.shadow") version "6.0.0"
|
||||
}
|
||||
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
val jar by tasks.getting(Jar::class) {
|
||||
manifest {
|
||||
attributes["Main-Class"] = "com.topjohnwu.signing.ZipSigner"
|
||||
}
|
||||
}
|
||||
|
||||
val shadowJar by tasks.getting(ShadowJar::class) {
|
||||
archiveBaseName.set("zipsigner")
|
||||
archiveClassifier.set(null as String?)
|
||||
archiveVersion.set("4.0")
|
||||
}
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
|
||||
api("org.bouncycastle:bcprov-jdk15on:1.66")
|
||||
api("org.bouncycastle:bcpkix-jdk15on:1.66")
|
||||
}
|
@@ -1,49 +0,0 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class BootSigner {
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
||||
String certPath = "";
|
||||
if (args.length >= 2) {
|
||||
/* args[1] is the path to a public key certificate */
|
||||
certPath = args[1];
|
||||
}
|
||||
boolean signed = SignBoot.verifySignature(System.in,
|
||||
certPath.isEmpty() ? null : new FileInputStream(certPath));
|
||||
System.exit(signed ? 0 : 1);
|
||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
||||
InputStream cert = null;
|
||||
InputStream key = null;
|
||||
String name = "/boot";
|
||||
|
||||
if (args.length >= 3) {
|
||||
cert = new FileInputStream(args[1]);
|
||||
key = new FileInputStream(args[2]);
|
||||
}
|
||||
if (args.length == 2) {
|
||||
name = args[1];
|
||||
} else if (args.length >= 4) {
|
||||
name = args[3];
|
||||
}
|
||||
|
||||
boolean success = SignBoot.doSignature(name, System.in, System.out, cert, key);
|
||||
System.exit(success ? 0 : 1);
|
||||
} else {
|
||||
System.err.println(
|
||||
"BootSigner <actions> [args]\n" +
|
||||
"Input from stdin, outputs to stdout\n" +
|
||||
"\n" +
|
||||
"Actions:\n" +
|
||||
" -verify [x509.pem]\n" +
|
||||
" verify image, cert is optional\n" +
|
||||
" -sign [x509.pem] [pk8] [name]\n" +
|
||||
" sign image, name, cert and key pair are optional\n" +
|
||||
" name should be /boot (default) or /recovery\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Security;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class ZipSigner {
|
||||
|
||||
private static void usage() {
|
||||
System.err.println("ZipSigner usage:");
|
||||
System.err.println(" zipsigner.jar input.jar output.jar");
|
||||
System.err.println(" sign jar with AOSP test keys");
|
||||
System.err.println(" zipsigner.jar x509.pem pk8 input.jar output.jar");
|
||||
System.err.println(" sign jar with certificate / private key pair");
|
||||
System.err.println(" zipsigner.jar keyStore keyStorePass alias keyPass input.jar output.jar");
|
||||
System.err.println(" sign jar with Java KeyStore");
|
||||
System.exit(2);
|
||||
}
|
||||
|
||||
private static void sign(JarMap input, FileOutputStream output) throws Exception {
|
||||
sign(SignApk.class.getResourceAsStream("/keys/testkey.x509.pem"),
|
||||
SignApk.class.getResourceAsStream("/keys/testkey.pk8"), input, output);
|
||||
}
|
||||
|
||||
private static void sign(InputStream certIs, InputStream keyIs,
|
||||
JarMap input, FileOutputStream output) throws Exception {
|
||||
X509Certificate cert = CryptoUtils.readCertificate(certIs);
|
||||
PrivateKey key = CryptoUtils.readPrivateKey(keyIs);
|
||||
SignApk.sign(cert, key, input, output);
|
||||
}
|
||||
|
||||
private static void sign(String keyStore, String keyStorePass, String alias, String keyPass,
|
||||
JarMap in, FileOutputStream out) throws Exception {
|
||||
KeyStore ks;
|
||||
try {
|
||||
ks = KeyStore.getInstance("JKS");
|
||||
try (InputStream is = new FileInputStream(keyStore)) {
|
||||
ks.load(is, keyStorePass.toCharArray());
|
||||
}
|
||||
} catch (KeyStoreException|IOException|CertificateException|NoSuchAlgorithmException e) {
|
||||
ks = KeyStore.getInstance("PKCS12");
|
||||
try (InputStream is = new FileInputStream(keyStore)) {
|
||||
ks.load(is, keyStorePass.toCharArray());
|
||||
}
|
||||
}
|
||||
X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
|
||||
PrivateKey key = (PrivateKey) ks.getKey(alias, keyPass.toCharArray());
|
||||
SignApk.sign(cert, key, in, out);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length != 2 && args.length != 4 && args.length != 6)
|
||||
usage();
|
||||
|
||||
Security.insertProviderAt(new BouncyCastleProvider(), 1);
|
||||
|
||||
try (JarMap in = JarMap.open(args[args.length - 2], false);
|
||||
FileOutputStream out = new FileOutputStream(args[args.length - 1])) {
|
||||
if (args.length == 2) {
|
||||
sign(in, out);
|
||||
} else if (args.length == 4) {
|
||||
try (InputStream cert = new FileInputStream(args[0]);
|
||||
InputStream key = new FileInputStream(args[1])) {
|
||||
sign(cert, key, in, out);
|
||||
}
|
||||
} else if (args.length == 6) {
|
||||
sign(args[0], args[1], args[2], args[3], in, out);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -4,16 +4,15 @@
|
||||
package="com.topjohnwu.magisk">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
android:maxSdkVersion="29" />
|
||||
|
||||
<application
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:name="a.e"
|
||||
android:allowBackup="true"
|
||||
android:allowBackup="false"
|
||||
tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
|
||||
|
||||
<!-- Splash -->
|
||||
@@ -65,6 +64,15 @@
|
||||
<!-- DownloadService -->
|
||||
<service android:name="a.j" />
|
||||
|
||||
<!-- FileProvider -->
|
||||
<provider
|
||||
android:name="a.p"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:directBootAware="true"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
</provider>
|
||||
|
||||
<!-- Hardcode GMS version -->
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.version"
|
||||
|
@@ -2,15 +2,16 @@
|
||||
package a
|
||||
|
||||
import com.topjohnwu.magisk.core.App
|
||||
import com.topjohnwu.magisk.core.GeneralReceiver
|
||||
import com.topjohnwu.magisk.core.Provider
|
||||
import com.topjohnwu.magisk.core.Receiver
|
||||
import com.topjohnwu.magisk.core.SplashActivity
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.ui.surequest.SuRequestActivity
|
||||
import com.topjohnwu.signing.BootSigner
|
||||
import com.topjohnwu.signing.SignBoot
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
BootSigner.main(args)
|
||||
SignBoot.main(args)
|
||||
}
|
||||
|
||||
class b : MainActivity()
|
||||
@@ -22,8 +23,10 @@ class e : App {
|
||||
constructor(o: Any) : super(o)
|
||||
}
|
||||
|
||||
class h : GeneralReceiver()
|
||||
class h : Receiver()
|
||||
|
||||
class j : DownloadService()
|
||||
|
||||
class m : SuRequestActivity()
|
||||
|
||||
class p : Provider()
|
||||
|
@@ -1,5 +1,8 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
@@ -14,6 +17,7 @@ import androidx.navigation.fragment.NavHostFragment
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.ui.inflater.LayoutInflaterFactory
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
|
||||
abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
@@ -41,6 +45,8 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
layoutInflater.factory2 = LayoutInflaterFactory(delegate)
|
||||
|
||||
setTheme(themeRes)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -59,6 +65,31 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
directionsDispatcher.value = null
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
window?.decorView?.let {
|
||||
it.systemUiVisibility = (it.systemUiVisibility
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
window?.decorView?.post {
|
||||
// If navigation bar is short enough (gesture navigation enabled), make it transparent
|
||||
if (window.decorView.rootWindowInsets?.systemWindowInsetBottom ?: 0 < Resources.getSystem().displayMetrics.density * 40) {
|
||||
window.navigationBarColor = Color.TRANSPARENT
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
window.isStatusBarContrastEnforced = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setContentView() {
|
||||
@@ -66,8 +97,10 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
it.setVariable(BR.viewModel, viewModel)
|
||||
it.lifecycleOwner = this
|
||||
}
|
||||
}
|
||||
|
||||
ensureInsets()
|
||||
fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
|
||||
viewRoot.rootView.accessibilityDelegate = delegate
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@@ -1,9 +1,6 @@
|
||||
package com.topjohnwu.magisk.arch
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
|
||||
@@ -17,47 +14,8 @@ interface BaseUIComponent<VM : BaseViewModel>: LifecycleOwner {
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeSystemWindowInsets(insets: Insets): Insets? = null
|
||||
|
||||
/**
|
||||
* Called for all [ViewEvent]s published by associated viewModel.
|
||||
*/
|
||||
fun onEventDispatched(event: ViewEvent) {}
|
||||
|
||||
fun ensureInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(viewRoot) { _, insets ->
|
||||
insets.asInsets()
|
||||
.also { viewModel.insets = it }
|
||||
.let { consumeSystemWindowInsets(it) }
|
||||
?.subtractBy(insets) ?: insets
|
||||
}
|
||||
if (ViewCompat.isAttachedToWindow(viewRoot)) {
|
||||
ViewCompat.requestApplyInsets(viewRoot)
|
||||
} else {
|
||||
viewRoot.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
ViewCompat.requestApplyInsets(v)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun WindowInsetsCompat.asInsets() = Insets.of(
|
||||
systemWindowInsetLeft,
|
||||
systemWindowInsetTop,
|
||||
systemWindowInsetRight,
|
||||
systemWindowInsetBottom
|
||||
)
|
||||
|
||||
private fun Insets.subtractBy(insets: WindowInsetsCompat) =
|
||||
WindowInsetsCompat.Builder(insets).setSystemWindowInsets(
|
||||
Insets.of(
|
||||
insets.systemWindowInsetLeft - left,
|
||||
insets.systemWindowInsetTop - top,
|
||||
insets.systemWindowInsetRight - right,
|
||||
insets.systemWindowInsetBottom - bottom
|
||||
)
|
||||
).build()
|
||||
|
||||
}
|
||||
|
@@ -24,8 +24,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
override val viewRoot: View get() = binding.root
|
||||
private val navigation get() = activity.navigation
|
||||
|
||||
override fun consumeSystemWindowInsets(insets: Insets) = insets
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
startObserveEvents()
|
||||
@@ -43,6 +41,11 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
activity.supportActionBar?.subtitle = null
|
||||
}
|
||||
|
||||
override fun onEventDispatched(event: ViewEvent) = when(event) {
|
||||
is ContextExecutor -> event(requireContext())
|
||||
is ActivityExecutor -> event(activity)
|
||||
@@ -65,7 +68,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
|
||||
return true
|
||||
}
|
||||
})
|
||||
ensureInsets()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@@ -9,18 +9,18 @@ import me.tatarka.bindingcollectionadapter2.BindingRecyclerViewAdapter
|
||||
import me.tatarka.bindingcollectionadapter2.ItemBinding
|
||||
import me.tatarka.bindingcollectionadapter2.OnItemBind
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> diffListOf(
|
||||
fun <T : ComparableRvItem<*>> diffListOf(
|
||||
vararg newItems: T
|
||||
) = diffListOf(newItems.toList())
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> diffListOf(
|
||||
fun <T : ComparableRvItem<*>> diffListOf(
|
||||
newItems: List<T>
|
||||
) = DiffObservableList(object : DiffObservableList.Callback<T> {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
||||
override fun areContentsTheSame(oldItem: T, newItem: T) = oldItem.genericContentSameAs(newItem)
|
||||
}).also { it.update(newItems) }
|
||||
|
||||
inline fun <T : ComparableRvItem<*>> filterableListOf(
|
||||
fun <T : ComparableRvItem<*>> filterableListOf(
|
||||
vararg newItems: T
|
||||
) = FilterableDiffObservableList(object : DiffObservableList.Callback<T> {
|
||||
override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem.genericItemSameAs(newItem)
|
||||
|
@@ -10,8 +10,6 @@ import androidx.multidex.MultiDex
|
||||
import androidx.work.WorkManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import com.topjohnwu.magisk.core.utils.IODispatcherExecutor
|
||||
import com.topjohnwu.magisk.core.utils.RootInit
|
||||
import com.topjohnwu.magisk.core.utils.updateConfig
|
||||
@@ -36,7 +34,6 @@ open class App() : Application() {
|
||||
.setInitializers(RootInit::class.java)
|
||||
.setTimeout(2))
|
||||
Shell.EXECUTOR = IODispatcherExecutor()
|
||||
FileProvider.callHandler = SuCallbackHandler
|
||||
|
||||
// Always log full stack trace with Timber
|
||||
Timber.plant(Timber.DebugTree())
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Xml
|
||||
@@ -9,19 +10,15 @@ import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.magiskdb.SettingsDao
|
||||
import com.topjohnwu.magisk.core.magiskdb.StringDao
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.defaultLocale
|
||||
import com.topjohnwu.magisk.core.utils.refreshLocale
|
||||
import com.topjohnwu.magisk.data.preference.PreferenceModel
|
||||
import com.topjohnwu.magisk.data.repository.DBConfig
|
||||
import com.topjohnwu.magisk.di.Protected
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.inject
|
||||
import com.topjohnwu.magisk.ui.theme.Theme
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import com.topjohnwu.superuser.io.SuFileInputStream
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
object Config : PreferenceModel, DBConfig {
|
||||
|
||||
@@ -29,6 +26,15 @@ object Config : PreferenceModel, DBConfig {
|
||||
override val settingsDao: SettingsDao by inject()
|
||||
override val context: Context by inject(Protected)
|
||||
|
||||
@get:SuppressLint("ApplySharedPref")
|
||||
val prefsFile: File get() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply {
|
||||
remove(Key.ASKED_HOME)
|
||||
}.commit()
|
||||
return File("${context.filesDir.parent}/shared_prefs", "${fileName}.xml")
|
||||
}
|
||||
|
||||
object Key {
|
||||
// db configs
|
||||
const val ROOT_ACCESS = "root_access"
|
||||
@@ -43,6 +49,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
const val SU_AUTO_RESPONSE = "su_auto_response"
|
||||
const val SU_NOTIFICATION = "su_notification"
|
||||
const val SU_REAUTH = "su_reauth"
|
||||
const val SU_TAPJACK = "su_tapjack"
|
||||
const val CHECK_UPDATES = "check_update"
|
||||
const val UPDATE_CHANNEL = "update_channel"
|
||||
const val CUSTOM_CHANNEL = "custom_channel"
|
||||
@@ -119,7 +126,7 @@ object Config : PreferenceModel, DBConfig {
|
||||
var repoOrder by preference(Key.REPO_ORDER, Value.ORDER_DATE)
|
||||
|
||||
var suDefaultTimeout by preferenceStrInt(Key.SU_REQUEST_TIMEOUT, 10)
|
||||
var suAutoReponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suAutoResponse by preferenceStrInt(Key.SU_AUTO_RESPONSE, Value.SU_PROMPT)
|
||||
var suNotification by preferenceStrInt(Key.SU_NOTIFICATION, Value.NOTIFICATION_TOAST)
|
||||
var updateChannel by preferenceStrInt(Key.UPDATE_CHANNEL, defaultChannel)
|
||||
|
||||
@@ -127,8 +134,9 @@ object Config : PreferenceModel, DBConfig {
|
||||
var darkTheme by preference(Key.DARK_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
var themeOrdinal by preference(Key.THEME_ORDINAL, Theme.Piplup.ordinal)
|
||||
var suReAuth by preference(Key.SU_REAUTH, false)
|
||||
var suTapjack by preference(Key.SU_TAPJACK, true)
|
||||
var checkUpdate by preference(Key.CHECK_UPDATES, true)
|
||||
var doh by preference(Key.DOH, defaultLocale.country == "CN")
|
||||
var doh by preference(Key.DOH, false)
|
||||
var magiskHide by preference(Key.MAGISKHIDE, true)
|
||||
var showSystemApp by preference(Key.SHOW_SYSTEM_APP, false)
|
||||
|
||||
@@ -150,8 +158,13 @@ object Config : PreferenceModel, DBConfig {
|
||||
|
||||
private const val SU_FINGERPRINT = "su_fingerprint"
|
||||
|
||||
fun initialize() {
|
||||
prefs.edit { parsePrefs() }
|
||||
fun load(pkg: String?) {
|
||||
// Only try to load prefs when fresh install and a previous package name is set
|
||||
if (pkg != null && prefs.all.isEmpty()) runCatching {
|
||||
context.contentResolver.openInputStream(Provider.PREFS_URI(pkg))?.use {
|
||||
prefs.edit { parsePrefs(it) }
|
||||
}
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
// Settings migration
|
||||
@@ -173,10 +186,8 @@ object Config : PreferenceModel, DBConfig {
|
||||
}
|
||||
}
|
||||
|
||||
private fun SharedPreferences.Editor.parsePrefs() {
|
||||
val config = SuFile.open("/data/adb", Const.MANAGER_CONFIGS)
|
||||
if (config.exists()) runCatching {
|
||||
val input = SuFileInputStream(config)
|
||||
private fun SharedPreferences.Editor.parsePrefs(input: InputStream) {
|
||||
runCatching {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false)
|
||||
parser.setInput(input, "UTF-8")
|
||||
@@ -220,21 +231,6 @@ object Config : PreferenceModel, DBConfig {
|
||||
else -> parser.next()
|
||||
}
|
||||
}
|
||||
config.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun export() {
|
||||
// Flush prefs to disk
|
||||
prefs.edit().apply {
|
||||
remove(Key.ASKED_HOME)
|
||||
}.commit()
|
||||
val context = get<Context>(Protected)
|
||||
val xml = File(
|
||||
"${context.filesDir.parent}/shared_prefs",
|
||||
"${context.packageName}_preferences.xml"
|
||||
)
|
||||
Shell.su("cat $xml > /data/adb/${Const.MANAGER_CONFIGS}").exec()
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -16,9 +16,6 @@ object Const {
|
||||
const val BOOTCTL_REVISION = "18ab78817087c337ae0edd1ecac38aec49217880"
|
||||
|
||||
// Misc
|
||||
const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
const val MAGISK_INSTALL_LOG_FILENAME = "magisk_install_log_%s.log"
|
||||
const val MANAGER_CONFIGS = ".tmp.magisk.config"
|
||||
val USER_ID = Process.myUid() / 100000
|
||||
|
||||
object Version {
|
||||
@@ -28,6 +25,7 @@ object Const {
|
||||
fun atLeast_20_2() = Info.env.magiskVersionCode >= 20200 || isCanary()
|
||||
fun atLeast_20_4() = Info.env.magiskVersionCode >= 20400 || isCanary()
|
||||
fun atLeast_21_0() = Info.env.magiskVersionCode >= 21000 || isCanary()
|
||||
fun atLeast_21_2() = Info.env.magiskVersionCode >= 21200 || isCanary()
|
||||
fun isCanary() = Info.env.magiskVersionCode % 100 != 0
|
||||
}
|
||||
|
||||
@@ -39,14 +37,12 @@ object Const {
|
||||
// notifications
|
||||
const val MAGISK_UPDATE_NOTIFICATION_ID = 4
|
||||
const val APK_UPDATE_NOTIFICATION_ID = 5
|
||||
const val HIDE_MANAGER_NOTIFICATION_ID = 8
|
||||
const val UPDATE_NOTIFICATION_CHANNEL = "update"
|
||||
const val PROGRESS_NOTIFICATION_CHANNEL = "progress"
|
||||
const val CHECK_MAGISK_UPDATE_WORKER_ID = "magisk_update"
|
||||
}
|
||||
|
||||
object Url {
|
||||
const val ZIP_URL = "https://github.com/Magisk-Modules-Repo/%s/archive/master.zip"
|
||||
const val PATREON_URL = "https://www.patreon.com/topjohnwu"
|
||||
const val SOURCE_CODE_URL = "https://github.com/topjohnwu/Magisk"
|
||||
|
||||
@@ -54,14 +50,13 @@ object Const {
|
||||
const val GITHUB_API_URL = "https://api.github.com/"
|
||||
const val GITHUB_PAGE_URL = "https://topjohnwu.github.io/magisk_files/"
|
||||
const val JS_DELIVR_URL = "https://cdn.jsdelivr.net/gh/"
|
||||
const val OFFICIAL_REPO = "https://magisk-modules-repo.github.io/submission/modules.json"
|
||||
}
|
||||
|
||||
object Key {
|
||||
// others
|
||||
const val LINK_KEY = "Link"
|
||||
const val ETAG_KEY = "ETag"
|
||||
// intents
|
||||
const val OPEN_SECTION = "section"
|
||||
const val PREV_PKG = "prev_pkg"
|
||||
}
|
||||
|
||||
object Value {
|
||||
|
@@ -149,7 +149,7 @@ private object ClassMap {
|
||||
App::class.java to a.e::class.java,
|
||||
MainActivity::class.java to a.b::class.java,
|
||||
SplashActivity::class.java to a.c::class.java,
|
||||
GeneralReceiver::class.java to a.h::class.java,
|
||||
Receiver::class.java to a.h::class.java,
|
||||
DownloadService::class.java to a.j::class.java,
|
||||
SuRequestActivity::class.java to a.m::class.java
|
||||
)
|
||||
|
39
app/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
39
app/src/main/java/com/topjohnwu/magisk/core/Provider.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
package com.topjohnwu.magisk.core
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ProviderInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.ParcelFileDescriptor
|
||||
import android.os.ParcelFileDescriptor.MODE_READ_ONLY
|
||||
import com.topjohnwu.magisk.FileProvider
|
||||
import com.topjohnwu.magisk.core.su.SuCallbackHandler
|
||||
import java.io.File
|
||||
|
||||
open class Provider : FileProvider() {
|
||||
|
||||
override fun attachInfo(context: Context, info: ProviderInfo?) {
|
||||
super.attachInfo(context.wrap(), info)
|
||||
}
|
||||
|
||||
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
SuCallbackHandler(context!!, method, extras)
|
||||
return Bundle.EMPTY
|
||||
}
|
||||
|
||||
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
|
||||
return when (uri.encodedPath ?: return null) {
|
||||
"/apk_file" -> ParcelFileDescriptor.open(File(context!!.packageCodePath), MODE_READ_ONLY)
|
||||
"/prefs_file" -> ParcelFileDescriptor.open(Config.prefsFile, MODE_READ_ONLY)
|
||||
else -> super.openFile(uri, mode)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun APK_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("apk_file").build()
|
||||
|
||||
fun PREFS_URI(pkg: String) =
|
||||
Uri.Builder().scheme("content").authority("$pkg.provider").path("prefs_file").build()
|
||||
}
|
||||
}
|
@@ -11,7 +11,7 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.inject
|
||||
|
||||
open class GeneralReceiver : BaseReceiver() {
|
||||
open class Receiver : BaseReceiver() {
|
||||
|
||||
private val policyDB: PolicyDao by inject()
|
||||
|
@@ -3,9 +3,9 @@ package com.topjohnwu.magisk.core
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
@@ -29,18 +29,18 @@ open class SplashActivity : Activity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRepackage() {
|
||||
val pkg = Config.suManager
|
||||
if (Config.suManager.isNotEmpty() && packageName == BuildConfig.APPLICATION_ID) {
|
||||
Config.suManager = ""
|
||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
if (pkg == packageName) {
|
||||
private fun handleRepackage(pkg: String?) {
|
||||
if (packageName != APPLICATION_ID) {
|
||||
runCatching {
|
||||
// We are the manager, remove com.topjohnwu.magisk as it could be malware
|
||||
packageManager.getApplicationInfo(BuildConfig.APPLICATION_ID, 0)
|
||||
Shell.su("(pm uninstall ${BuildConfig.APPLICATION_ID})& >/dev/null 2>&1").exec()
|
||||
// Hidden, remove com.topjohnwu.magisk if exist as it could be malware
|
||||
packageManager.getApplicationInfo(APPLICATION_ID, 0)
|
||||
Shell.su("(pm uninstall $APPLICATION_ID)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
} else {
|
||||
if (Config.suManager.isNotEmpty())
|
||||
Config.suManager = ""
|
||||
pkg ?: return
|
||||
Shell.su("(pm uninstall $pkg)& >/dev/null 2>&1").exec()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,14 +48,16 @@ open class SplashActivity : Activity() {
|
||||
// Pre-initialize root shell
|
||||
Shell.getShell()
|
||||
|
||||
Config.initialize()
|
||||
handleRepackage()
|
||||
val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
|
||||
|
||||
Config.load(prevPkg)
|
||||
handleRepackage(prevPkg)
|
||||
Notifications.setup(this)
|
||||
UpdateCheckService.schedule(this)
|
||||
Shortcuts.setupDynamic(this)
|
||||
|
||||
// Pre-fetch network stuffs
|
||||
get<GithubRawServices>()
|
||||
// Pre-fetch network services
|
||||
get<NetworkService>()
|
||||
|
||||
DONE = true
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package com.topjohnwu.magisk.core.base
|
||||
|
||||
import android.Manifest
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
@@ -40,8 +40,8 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
fun withPermission(permission: String, builder: PermissionRequestBuilder.() -> Unit) {
|
||||
val request = PermissionRequestBuilder().apply(builder).build()
|
||||
|
||||
if (permission == Manifest.permission.WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 29) {
|
||||
// We do not need external rw on 29+
|
||||
if (permission == WRITE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= 30) {
|
||||
// We do not need external rw on 30+
|
||||
request.onSuccess()
|
||||
return
|
||||
}
|
||||
@@ -52,7 +52,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
var requestCode: Int
|
||||
do {
|
||||
requestCode = Random.nextInt(Const.ID.MAX_ACTIVITY_RESULT + 1, 1 shl 15)
|
||||
} while (!resultCallbacks.containsKey(requestCode))
|
||||
} while (resultCallbacks.containsKey(requestCode))
|
||||
resultCallbacks[requestCode] = { result, _ ->
|
||||
if (result > 0)
|
||||
request.onSuccess()
|
||||
@@ -64,7 +64,7 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
fun withExternalRW(builder: PermissionRequestBuilder.() -> Unit) {
|
||||
withPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
withPermission(WRITE_EXTERNAL_STORAGE, builder = builder)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
|
@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.download
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
sealed class Action : Parcelable {
|
||||
|
||||
@@ -16,15 +16,6 @@ sealed class Action : Parcelable {
|
||||
|
||||
}
|
||||
|
||||
sealed class APK : Action() {
|
||||
|
||||
@Parcelize
|
||||
object Upgrade : APK()
|
||||
|
||||
@Parcelize
|
||||
object Restore : APK()
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
object Download : Action()
|
||||
|
||||
|
@@ -117,7 +117,7 @@ abstract class BaseDownloader : BaseService(), KoinComponent {
|
||||
fun Subject.notifyID() = hashCode()
|
||||
|
||||
private fun notifyFail(subject: Subject) = lastNotify(subject.notifyID()) {
|
||||
broadcast(-1f, subject)
|
||||
broadcast(-2f, subject)
|
||||
it.setContentText(getString(R.string.download_file_error))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setOngoing(false)
|
||||
|
@@ -73,10 +73,7 @@ open class DownloadService : BaseDownloader() {
|
||||
}
|
||||
|
||||
private fun Notification.Builder.setIntent(subject: Manager)
|
||||
= when (subject.action) {
|
||||
APK.Upgrade -> setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
||||
else -> setContentIntent(Intent())
|
||||
}
|
||||
= setContentIntent(APKInstall.installIntent(context, subject.file.toFile()))
|
||||
|
||||
private fun Notification.Builder.setContentIntent(intent: Intent) =
|
||||
setContentIntent(
|
||||
|
@@ -5,20 +5,16 @@ import androidx.core.net.toFile
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Action.APK.Restore
|
||||
import com.topjohnwu.magisk.core.download.Action.APK.Upgrade
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.ktx.relaunchApp
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import java.io.File
|
||||
|
||||
private fun Context.patch(apk: File) {
|
||||
val patched = File(apk.parent, "patched.apk")
|
||||
PatchAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||
HideAPK.patch(this, apk.path, patched.path, packageName, applicationInfo.nonLocalizedLabel)
|
||||
apk.delete()
|
||||
patched.renameTo(apk)
|
||||
}
|
||||
@@ -31,7 +27,7 @@ private fun BaseDownloader.notifyHide(id: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
|
||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) {
|
||||
val apk = subject.file.toFile()
|
||||
val id = subject.notifyID()
|
||||
if (isRunningAsStub) {
|
||||
@@ -53,20 +49,3 @@ private suspend fun BaseDownloader.upgrade(subject: Subject.Manager) {
|
||||
patch(apk)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BaseDownloader.restore(apk: File, id: Int) {
|
||||
update(id) {
|
||||
it.setProgress(0, 0, true)
|
||||
.setProgress(0, 0, true)
|
||||
.setContentTitle(getString(R.string.restore_img_msg))
|
||||
.setContentText("")
|
||||
}
|
||||
Config.export()
|
||||
Shell.su("pm install $apk && pm uninstall $packageName").exec()
|
||||
}
|
||||
|
||||
suspend fun BaseDownloader.handleAPK(subject: Subject.Manager) =
|
||||
when (subject.action) {
|
||||
is Upgrade -> upgrade(subject)
|
||||
is Restore -> restore(subject.file.toFile(), subject.notifyID())
|
||||
}
|
||||
|
@@ -8,12 +8,12 @@ import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.MagiskJson
|
||||
import com.topjohnwu.magisk.core.model.ManagerJson
|
||||
import com.topjohnwu.magisk.core.model.StubJson
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.ktx.cachedFile
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import kotlinx.android.parcel.IgnoredOnParcel
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
private fun cachedFile(name: String) = get<Context>().cachedFile(name).apply { delete() }.toUri()
|
||||
|
||||
@@ -26,10 +26,10 @@ sealed class Subject : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
class Module(
|
||||
val module: Repo,
|
||||
val module: OnlineModule,
|
||||
override val action: Action
|
||||
) : Subject() {
|
||||
override val url: String get() = module.zipUrl
|
||||
override val url: String get() = module.zip_url
|
||||
override val title: String get() = module.downloadFilename
|
||||
|
||||
@IgnoredOnParcel
|
||||
@@ -40,16 +40,12 @@ sealed class Subject : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
class Manager(
|
||||
override val action: Action.APK,
|
||||
private val app: ManagerJson = Info.remote.app,
|
||||
val stub: StubJson = Info.remote.stub
|
||||
) : Subject() {
|
||||
|
||||
override val title: String
|
||||
get() = "MagiskManager-${app.version}(${app.versionCode})"
|
||||
|
||||
override val url: String
|
||||
get() = app.link
|
||||
override val action get() = Action.Download
|
||||
override val title: String get() = "MagiskManager-${app.version}(${app.versionCode})"
|
||||
override val url: String get() = app.link
|
||||
|
||||
@IgnoredOnParcel
|
||||
override val file by lazy {
|
||||
@@ -105,7 +101,6 @@ sealed class Subject : Parcelable {
|
||||
Action.Download -> Download()
|
||||
Action.Uninstall -> Uninstall()
|
||||
Action.EnvFix, is Action.Flash, is Action.Patch -> Internal(config)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,7 @@ package com.topjohnwu.magisk.core.model
|
||||
|
||||
import android.os.Parcelable
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateInfo(
|
||||
@@ -41,3 +41,29 @@ data class StubJson(
|
||||
val versionCode: Int = -1,
|
||||
val link: String = ""
|
||||
) : Parcelable
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ModuleJson(
|
||||
val id: String,
|
||||
val last_update: Long,
|
||||
val prop_url: String,
|
||||
val zip_url: String,
|
||||
val notes_url: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class RepoJson(
|
||||
val name: String,
|
||||
val last_update: Long,
|
||||
val modules: List<ModuleJson>
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CommitInfo(
|
||||
val sha: String
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BranchInfo(
|
||||
val commit: CommitInfo
|
||||
)
|
||||
|
@@ -1,41 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
abstract class BaseModule : Comparable<BaseModule> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: BaseModule) = name.compareTo(other.name, true)
|
||||
}
|
@@ -0,0 +1,89 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class LocalModule(path: String) : Module() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
|
||||
val updated: Boolean get() = updateFile.exists()
|
||||
|
||||
var enable: Boolean
|
||||
get() = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
if (enable) {
|
||||
disableFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
} else {
|
||||
!disableFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $dir").submit()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean
|
||||
get() = removeFile.exists()
|
||||
set(remove) {
|
||||
if (remove) {
|
||||
removeFile.createNewFile()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
} else {
|
||||
!removeFile.delete()
|
||||
if (Const.Version.atLeast_21_2())
|
||||
Shell.su("copy_sepolicy_rules").submit()
|
||||
else
|
||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||
|
||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||
SuFile(Const.MAGISK_PATH)
|
||||
.listFiles { _, name -> name != "lost+found" && name != ".core" }
|
||||
.orEmpty()
|
||||
.filter { !it.isFile }
|
||||
.map { LocalModule("${Const.MAGISK_PATH}/${it.name}") }
|
||||
.sortedBy { it.name.toLowerCase() }
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,77 +1,41 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import com.topjohnwu.superuser.io.SuFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
abstract class Module : Comparable<Module> {
|
||||
abstract var id: String
|
||||
protected set
|
||||
abstract var name: String
|
||||
protected set
|
||||
abstract var author: String
|
||||
protected set
|
||||
abstract var version: String
|
||||
protected set
|
||||
abstract var versionCode: Int
|
||||
protected set
|
||||
abstract var description: String
|
||||
protected set
|
||||
|
||||
class Module(path: String) : BaseModule() {
|
||||
override var id: String = ""
|
||||
override var name: String = ""
|
||||
override var author: String = ""
|
||||
override var version: String = ""
|
||||
override var versionCode: Int = -1
|
||||
override var description: String = ""
|
||||
@Throws(NumberFormatException::class)
|
||||
protected fun parseProps(props: List<String>) {
|
||||
for (line in props) {
|
||||
val prop = line.split("=".toRegex(), 2).map { it.trim() }
|
||||
if (prop.size != 2)
|
||||
continue
|
||||
|
||||
private val removeFile = SuFile(path, "remove")
|
||||
private val disableFile = SuFile(path, "disable")
|
||||
private val updateFile = SuFile(path, "update")
|
||||
private val ruleFile = SuFile(path, "sepolicy.rule")
|
||||
val key = prop[0]
|
||||
val value = prop[1]
|
||||
if (key.isEmpty() || key[0] == '#')
|
||||
continue
|
||||
|
||||
val updated: Boolean get() = updateFile.exists()
|
||||
|
||||
var enable: Boolean
|
||||
get() = !disableFile.exists()
|
||||
set(enable) {
|
||||
val dir = "$PERSIST/$id"
|
||||
if (enable) {
|
||||
Shell.su("mkdir -p $dir", "cp -af $ruleFile $dir").submit()
|
||||
disableFile.delete()
|
||||
} else {
|
||||
Shell.su("rm -rf $dir").submit()
|
||||
!disableFile.createNewFile()
|
||||
}
|
||||
}
|
||||
|
||||
var remove: Boolean
|
||||
get() = removeFile.exists()
|
||||
set(remove) {
|
||||
if (remove) {
|
||||
Shell.su("rm -rf $PERSIST/$id").submit()
|
||||
removeFile.createNewFile()
|
||||
} else {
|
||||
Shell.su("cp -af $ruleFile $PERSIST/$id").submit()
|
||||
!removeFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
runCatching {
|
||||
parseProps(Shell.su("dos2unix < $path/module.prop").exec().out)
|
||||
}
|
||||
|
||||
if (id.isEmpty()) {
|
||||
val sep = path.lastIndexOf('/')
|
||||
id = path.substring(sep + 1)
|
||||
}
|
||||
|
||||
if (name.isEmpty()) {
|
||||
name = id
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val PERSIST get() = "${Const.MAGISKTMP}/mirror/persist/magisk"
|
||||
|
||||
suspend fun installed() = withContext(Dispatchers.IO) {
|
||||
SuFile(Const.MAGISK_PATH)
|
||||
.listFiles { _, name -> name != "lost+found" && name != ".core" }
|
||||
.orEmpty()
|
||||
.filter { !it.isFile }
|
||||
.map { Module("${Const.MAGISK_PATH}/${it.name}") }
|
||||
.sortedBy { it.name.toLowerCase() }
|
||||
when (key) {
|
||||
"id" -> id = value
|
||||
"name" -> name = value
|
||||
"version" -> version = value
|
||||
"versionCode" -> versionCode = value.toInt()
|
||||
"author" -> author = value
|
||||
"description" -> description = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override operator fun compareTo(other: Module) = name.compareTo(other.name, true)
|
||||
}
|
||||
|
@@ -0,0 +1,66 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.model.ModuleJson
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.legalFilename
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "modules")
|
||||
@Parcelize
|
||||
data class OnlineModule(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String = "",
|
||||
override var author: String = "",
|
||||
override var version: String = "",
|
||||
override var versionCode: Int = -1,
|
||||
override var description: String = "",
|
||||
val last_update: Long,
|
||||
val prop_url: String,
|
||||
val zip_url: String,
|
||||
val notes_url: String
|
||||
) : Module(), Parcelable {
|
||||
|
||||
private val svc: NetworkService get() = get()
|
||||
|
||||
constructor(info: ModuleJson) : this(
|
||||
id = info.id,
|
||||
last_update = info.last_update,
|
||||
prop_url = info.prop_url,
|
||||
zip_url = info.zip_url,
|
||||
notes_url = info.notes_url
|
||||
)
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
val lastUpdateString get() = DATE_FORMAT.format(lastUpdate)
|
||||
val downloadFilename get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
suspend fun notes() = svc.fetchString(notes_url)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
suspend fun load() {
|
||||
try {
|
||||
val rawProps = svc.fetchString(prop_url)
|
||||
val props = rawProps.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }
|
||||
parseProps(props)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalRepoException("Repo [$id] parse error:", e)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
class IllegalRepoException(msg: String, cause: Throwable? = null) : Exception(msg, cause)
|
||||
|
||||
companion object {
|
||||
private val DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||
}
|
||||
|
||||
}
|
@@ -1,64 +0,0 @@
|
||||
package com.topjohnwu.magisk.core.model.module
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.legalFilename
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
@Parcelize
|
||||
data class Repo(
|
||||
@PrimaryKey override var id: String,
|
||||
override var name: String,
|
||||
override var author: String,
|
||||
override var version: String,
|
||||
override var versionCode: Int,
|
||||
override var description: String,
|
||||
var last_update: Long
|
||||
) : BaseModule(), Parcelable {
|
||||
|
||||
private val svc: NetworkService get() = get()
|
||||
|
||||
val lastUpdate get() = Date(last_update)
|
||||
|
||||
val lastUpdateString: String get() = dateFormat.format(lastUpdate)
|
||||
|
||||
val downloadFilename: String get() = "$name-$version($versionCode).zip".legalFilename()
|
||||
|
||||
suspend fun readme() = svc.fetchReadme(this)
|
||||
|
||||
val zipUrl: String get() = Const.Url.ZIP_URL.format(id)
|
||||
|
||||
constructor(id: String) : this(id, "", "", "", -1, "", 0)
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
private fun loadProps(props: String) {
|
||||
props.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.runCatching {
|
||||
parseProps(this)
|
||||
}.onFailure {
|
||||
throw IllegalRepoException("Repo [$id] parse error: " + it.message)
|
||||
}
|
||||
|
||||
if (versionCode < 0) {
|
||||
throw IllegalRepoException("Repo [$id] does not contain versionCode")
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalRepoException::class)
|
||||
suspend fun update(lastUpdate: Date? = null) {
|
||||
lastUpdate?.let { last_update = it.time }
|
||||
loadProps(svc.fetchMetadata(this))
|
||||
}
|
||||
|
||||
class IllegalRepoException(message: String) : Exception(message)
|
||||
|
||||
companion object {
|
||||
val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
|
||||
}
|
||||
}
|
@@ -7,14 +7,12 @@ import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.ProviderCallHandler
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
import com.topjohnwu.magisk.core.model.su.SuPolicy
|
||||
import com.topjohnwu.magisk.core.model.su.toLog
|
||||
import com.topjohnwu.magisk.core.model.su.toPolicy
|
||||
import com.topjohnwu.magisk.core.wrap
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.startActivity
|
||||
@@ -26,18 +24,13 @@ import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
object SuCallbackHandler : ProviderCallHandler {
|
||||
object SuCallbackHandler {
|
||||
|
||||
const val REQUEST = "request"
|
||||
const val LOG = "log"
|
||||
const val NOTIFY = "notify"
|
||||
const val TEST = "test"
|
||||
|
||||
override fun call(context: Context, method: String, arg: String?, extras: Bundle?): Bundle? {
|
||||
invoke(context.wrap(), method, extras)
|
||||
return Bundle.EMPTY
|
||||
}
|
||||
|
||||
operator fun invoke(context: Context, action: String?, data: Bundle?) {
|
||||
data ?: return
|
||||
|
||||
|
@@ -36,7 +36,7 @@ class SuRequestHandler(
|
||||
if (policy.packageName == BuildConfig.APPLICATION_ID)
|
||||
return false
|
||||
|
||||
when (Config.suAutoReponse) {
|
||||
when (Config.suAutoResponse) {
|
||||
Config.Value.SU_AUTO_DENY -> {
|
||||
respond(SuPolicy.DENY, 0)
|
||||
return false
|
||||
|
@@ -1,20 +1,20 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.BuildConfig.APPLICATION_ID
|
||||
import com.topjohnwu.magisk.DynAPK
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.*
|
||||
import com.topjohnwu.magisk.core.utils.AXML
|
||||
import com.topjohnwu.magisk.core.utils.Keygen
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.inject
|
||||
import com.topjohnwu.magisk.ktx.writeTo
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.signing.JarMap
|
||||
import com.topjohnwu.signing.SignApk
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@@ -28,25 +28,28 @@ import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.security.SecureRandom
|
||||
|
||||
object PatchAPK {
|
||||
object HideAPK {
|
||||
|
||||
private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
|
||||
private const val ALPHADOTS = "$ALPHA....."
|
||||
|
||||
private const val APP_ID = "com.topjohnwu.magisk"
|
||||
private const val APP_NAME = "Magisk Manager"
|
||||
private const val ANDROID_MANIFEST = "AndroidManifest.xml"
|
||||
|
||||
// Some arbitrary limit
|
||||
const val MAX_LABEL_LENGTH = 32
|
||||
|
||||
private fun genPackageName(): CharSequence {
|
||||
private val svc: NetworkService by inject()
|
||||
private val Context.APK_URI get() = Provider.APK_URI(packageName)
|
||||
private val Context.PREFS_URI get() = Provider.PREFS_URI(packageName)
|
||||
|
||||
private fun genPackageName(): String {
|
||||
val random = SecureRandom()
|
||||
val len = 5 + random.nextInt(15)
|
||||
val builder = StringBuilder(len)
|
||||
var next: Char
|
||||
var prev = 0.toChar()
|
||||
for (i in 0 until len) {
|
||||
next = if (prev == '.' || i == len - 1) {
|
||||
next = if (prev == '.' || prev == 0.toChar() || i == len - 1) {
|
||||
ALPHA[random.nextInt(ALPHA.length)]
|
||||
} else {
|
||||
ALPHADOTS[random.nextInt(ALPHADOTS.length)]
|
||||
@@ -59,20 +62,20 @@ object PatchAPK {
|
||||
val idx = random.nextInt(len - 1)
|
||||
builder[idx] = '.'
|
||||
}
|
||||
return builder
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
fun patch(
|
||||
context: Context,
|
||||
apk: String, out: String,
|
||||
pkg: CharSequence, label: CharSequence
|
||||
pkg: String, label: CharSequence
|
||||
): Boolean {
|
||||
try {
|
||||
val jar = JarMap.open(apk)
|
||||
val je = jar.getJarEntry(Const.ANDROID_MANIFEST)
|
||||
val je = jar.getJarEntry(ANDROID_MANIFEST)
|
||||
val xml = AXML(jar.getRawData(je))
|
||||
|
||||
if (!xml.findAndPatch(APP_ID to pkg.toString(), APP_NAME to label.toString()))
|
||||
if (!xml.findAndPatch(APPLICATION_ID to pkg, APP_NAME to label.toString()))
|
||||
return false
|
||||
|
||||
// Write apk changes
|
||||
@@ -91,7 +94,6 @@ object PatchAPK {
|
||||
val dlStub = !isRunningAsStub && SDK_INT >= 28 && Const.Version.atLeast_20_2()
|
||||
val src = if (dlStub) {
|
||||
val stub = File(context.cacheDir, "stub.apk")
|
||||
val svc = get<NetworkService>()
|
||||
try {
|
||||
svc.fetchFile(Info.remote.stub.link).byteStream().use {
|
||||
it.writeTo(stub)
|
||||
@@ -117,23 +119,74 @@ object PatchAPK {
|
||||
if (!Shell.su("adb_pm_install $repack").exec().isSuccess)
|
||||
return false
|
||||
|
||||
Config.suManager = pkg.toString()
|
||||
Config.export()
|
||||
Shell.su("pm uninstall $APP_ID").submit()
|
||||
context.apply {
|
||||
val intent = packageManager.getLaunchIntentForPackage(pkg) ?: return false
|
||||
Config.suManager = pkg
|
||||
grantUriPermission(pkg, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
grantUriPermission(pkg, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun hideManager(context: Context, label: String) {
|
||||
val progress = Notifications.progress(context, context.getString(R.string.hide_manager_title))
|
||||
Notifications.mgr.notify(Const.ID.HIDE_MANAGER_NOTIFICATION_ID, progress.build())
|
||||
@Suppress("DEPRECATION")
|
||||
fun hide(context: Context, label: String) {
|
||||
val dialog = ProgressDialog.show(context, context.getString(R.string.hide_manager_title), "", true)
|
||||
GlobalScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
patchAndHide(context, label)
|
||||
}
|
||||
if (!result)
|
||||
if (!result) {
|
||||
Utils.toast(R.string.hide_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
Notifications.mgr.cancel(Const.ID.HIDE_MANAGER_NOTIFICATION_ID)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadAndRestore(context: Context): Boolean {
|
||||
val apk = if (isRunningAsStub) {
|
||||
DynAPK.current(context)
|
||||
} else {
|
||||
File(context.cacheDir, "manager.apk").also { apk ->
|
||||
try {
|
||||
svc.fetchFile(Info.remote.app.link).byteStream().use {
|
||||
it.writeTo(apk)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Shell.su("adb_pm_install $apk").exec().isSuccess)
|
||||
return false
|
||||
|
||||
context.apply {
|
||||
val intent = packageManager.getLaunchIntentForPackage(APPLICATION_ID) ?: return false
|
||||
Config.suManager = ""
|
||||
grantUriPermission(APPLICATION_ID, APK_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
grantUriPermission(APPLICATION_ID, PREFS_URI, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.putExtra(Const.Key.PREV_PKG, packageName)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
fun restore(context: Context) {
|
||||
val dialog = ProgressDialog.show(context, context.getString(R.string.restore_img_msg), "", true)
|
||||
GlobalScope.launch {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
downloadAndRestore(context)
|
||||
}
|
||||
if (!result) {
|
||||
Utils.toast(R.string.restore_manager_fail_toast, Toast.LENGTH_LONG)
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -8,8 +8,10 @@ import android.widget.Toast
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.inputStream
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
@@ -40,6 +42,8 @@ import org.koin.core.inject
|
||||
import timber.log.Timber
|
||||
import java.io.*
|
||||
import java.nio.ByteBuffer
|
||||
import java.security.SecureRandom
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
@@ -107,6 +111,8 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
}
|
||||
|
||||
console.add("- Device platform: " + Build.CPU_ABI)
|
||||
console.add("- Magisk Manager: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
console.add("- Install target: ${Info.remote.magisk.version} (${Info.remote.magisk.versionCode})")
|
||||
|
||||
try {
|
||||
ZipInputStream(zipUri.inputStream().buffered()).use { zi ->
|
||||
@@ -185,9 +191,10 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
if (rawData.size < 256)
|
||||
continue
|
||||
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
// Patch flags to AVB_VBMETA_IMAGE_FLAGS_HASHTREE_DISABLED |
|
||||
// AVB_VBMETA_IMAGE_FLAGS_VERIFICATION_DISABLED
|
||||
console.add("-- Patching: vbmeta.img")
|
||||
ByteBuffer.wrap(rawData).putInt(120, 2)
|
||||
ByteBuffer.wrap(rawData).putInt(120, 3)
|
||||
tarOut.putNextEntry(newEntry("vbmeta.img", rawData.size.toLong()))
|
||||
tarOut.write(rawData)
|
||||
} else {
|
||||
@@ -198,7 +205,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
}
|
||||
val boot = SuFile.open(installDir, "boot.img")
|
||||
val recovery = SuFile.open(installDir, "recovery.img")
|
||||
if (recovery.exists() && boot.exists()) {
|
||||
if (Config.recovery && recovery.exists() && boot.exists()) {
|
||||
// Install Magisk to recovery
|
||||
srcBoot = recovery.path
|
||||
// Repack boot image to prevent restore
|
||||
@@ -225,7 +232,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
|
||||
private fun handleFile(uri: Uri): Boolean {
|
||||
val outStream: OutputStream
|
||||
val outFile: MediaStoreUtils.UriFile
|
||||
var outFile: MediaStoreUtils.UriFile? = null
|
||||
|
||||
// Process input file
|
||||
try {
|
||||
@@ -237,27 +244,40 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
return false
|
||||
}
|
||||
src.reset()
|
||||
|
||||
val alpha = "abcdefghijklmnopqrstuvwxyz"
|
||||
val alphaNum = "$alpha${alpha.toUpperCase(Locale.ROOT)}0123456789"
|
||||
val random = SecureRandom()
|
||||
val suffix = StringBuilder()
|
||||
for (i in 1..5) {
|
||||
suffix.append(alphaNum[random.nextInt(alphaNum.length)])
|
||||
}
|
||||
|
||||
val filename = "magisk_patched_$suffix"
|
||||
outStream = if (magic.contentEquals("ustar".toByteArray())) {
|
||||
outFile = MediaStoreUtils.getFile("magisk_patched.tar")
|
||||
handleTar(src, outFile.uri.outputStream())
|
||||
outFile = MediaStoreUtils.getFile("$filename.tar", true)
|
||||
handleTar(src, outFile!!.uri.outputStream())
|
||||
} else {
|
||||
// Raw image
|
||||
srcBoot = File(installDir, "boot.img").path
|
||||
console.add("- Copying image to cache")
|
||||
FileOutputStream(srcBoot).use { src.copyTo(it) }
|
||||
outFile = MediaStoreUtils.getFile("magisk_patched.img")
|
||||
outFile.uri.outputStream()
|
||||
outFile = MediaStoreUtils.getFile("$filename.img", true)
|
||||
outFile!!.uri.outputStream()
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Process error")
|
||||
outFile?.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
|
||||
// Patch file
|
||||
if (!patchBoot())
|
||||
if (!patchBoot()) {
|
||||
outFile!!.delete()
|
||||
return false
|
||||
}
|
||||
|
||||
// Output file
|
||||
try {
|
||||
@@ -276,6 +296,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
console.add("****************************")
|
||||
} catch (e: IOException) {
|
||||
console.add("! Failed to output to $outFile")
|
||||
outFile!!.delete()
|
||||
Timber.e(e)
|
||||
return false
|
||||
}
|
||||
@@ -290,7 +311,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
srcBoot = File(installDir, "boot.img").path
|
||||
}
|
||||
|
||||
var isSigned = false
|
||||
var isSigned: Boolean
|
||||
try {
|
||||
SuFileInputStream(srcBoot).use {
|
||||
isSigned = SignBoot.verifySignature(it, null)
|
||||
@@ -325,7 +346,7 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
val signed = File(installDir, "signed.img")
|
||||
try {
|
||||
withStreams(SuFileInputStream(patched), signed.outputStream().buffered()) {
|
||||
input, out -> SignBoot.doSignature("/boot", input, out, null, null)
|
||||
input, out -> SignBoot.doSignature(null, null, input, out, "/boot")
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
console.add("! Unable to sign image")
|
||||
@@ -339,6 +360,13 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun copySepolicyRules(): Boolean {
|
||||
if (Info.remote.magisk.versionCode >= 21100) return true
|
||||
// Copy existing rules for migration
|
||||
"copy_sepolicy_rules".sh()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun flashBoot(): Boolean {
|
||||
if (!"direct_install $installDir $srcBoot".sh().isSuccess)
|
||||
return false
|
||||
@@ -373,10 +401,11 @@ abstract class MagiskInstallImpl : KoinComponent {
|
||||
|
||||
protected fun doPatchFile(patchFile: Uri) = extractZip() && handleFile(patchFile)
|
||||
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() && flashBoot()
|
||||
protected fun direct() = findImage() && extractZip() && patchBoot() &&
|
||||
copySepolicyRules() && flashBoot()
|
||||
|
||||
protected suspend fun secondSlot() =
|
||||
findSecondaryImage() && extractZip() && patchBoot() && flashBoot() && postOTA()
|
||||
protected suspend fun secondSlot() = findSecondaryImage() && extractZip() &&
|
||||
patchBoot() && copySepolicyRules() && flashBoot() && postOTA()
|
||||
|
||||
protected fun fixEnv(zip: Uri): Boolean {
|
||||
installDir = SuFile("/data/adb/magisk")
|
||||
|
@@ -1,119 +1,42 @@
|
||||
package com.topjohnwu.magisk.core.tasks
|
||||
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.data.repository.NetworkService
|
||||
import com.topjohnwu.magisk.ktx.synchronized
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.collections.HashSet
|
||||
|
||||
class RepoUpdater(
|
||||
private val svc: NetworkService,
|
||||
private val repoDB: RepoDao
|
||||
) {
|
||||
|
||||
private fun String.trimEtag() = substring(indexOf('\"'), lastIndexOf('\"') + 1)
|
||||
|
||||
private suspend fun forcedReload(cached: MutableSet<String>) = coroutineScope {
|
||||
cached.forEach {
|
||||
launch {
|
||||
val repo = repoDB.getRepo(it)!!
|
||||
try {
|
||||
repo.update()
|
||||
repoDB.addRepo(repo)
|
||||
} catch (e: Repo.IllegalRepoException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadRepos(
|
||||
repos: List<GithubRepoInfo>,
|
||||
cached: MutableSet<String>
|
||||
) = coroutineScope {
|
||||
repos.forEach {
|
||||
// Skip submission
|
||||
if (it.id == "submission")
|
||||
return@forEach
|
||||
launch {
|
||||
val repo = repoDB.getRepo(it.id)?.apply { cached.remove(it.id) } ?: Repo(it.id)
|
||||
try {
|
||||
repo.update(it.pushDate)
|
||||
repoDB.addRepo(repo)
|
||||
} catch (e: Repo.IllegalRepoException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class PageResult {
|
||||
SUCCESS,
|
||||
CACHED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private suspend fun loadPage(
|
||||
cached: MutableSet<String>,
|
||||
page: Int = 1,
|
||||
etag: String = ""
|
||||
): PageResult = coroutineScope {
|
||||
runCatching {
|
||||
val result = svc.fetchRepos(page, etag)
|
||||
result.run {
|
||||
if (code() == HttpURLConnection.HTTP_NOT_MODIFIED)
|
||||
return@coroutineScope PageResult.CACHED
|
||||
|
||||
if (!isSuccessful)
|
||||
return@coroutineScope PageResult.ERROR
|
||||
|
||||
if (page == 1)
|
||||
repoDB.etagKey = headers()[Const.Key.ETAG_KEY].orEmpty().trimEtag()
|
||||
|
||||
val repoLoad = async { loadRepos(body()!!, cached) }
|
||||
val next = if (headers()[Const.Key.LINK_KEY].orEmpty().contains("next")) {
|
||||
async { loadPage(cached, page + 1) }
|
||||
} else {
|
||||
async { PageResult.SUCCESS }
|
||||
}
|
||||
repoLoad.await()
|
||||
return@coroutineScope next.await()
|
||||
}
|
||||
}.getOrElse {
|
||||
Timber.e(it)
|
||||
PageResult.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun run(forced: Boolean) = withContext(Dispatchers.IO) {
|
||||
val cached = HashSet(repoDB.repoIDList).synchronized()
|
||||
when (loadPage(cached, etag = repoDB.etagKey)) {
|
||||
PageResult.CACHED -> if (forced) forcedReload(cached)
|
||||
PageResult.SUCCESS -> repoDB.removeRepos(cached)
|
||||
PageResult.ERROR -> Unit
|
||||
val cachedMap = HashMap<String, Date>().also { map ->
|
||||
repoDB.getModuleStubs().forEach { map[it.id] = Date(it.last_update) }
|
||||
}.synchronized()
|
||||
svc.fetchRepoInfo()?.let { info ->
|
||||
coroutineScope {
|
||||
info.modules.forEach {
|
||||
launch {
|
||||
val lastUpdated = cachedMap.remove(it.id)
|
||||
if (forced || lastUpdated?.before(Date(it.last_update)) != false) {
|
||||
try {
|
||||
val repo = OnlineModule(it).apply { load() }
|
||||
repoDB.addModule(repo)
|
||||
} catch (e: OnlineModule.IllegalRepoException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val dateFormat: SimpleDateFormat =
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GithubRepoInfo(
|
||||
val name: String,
|
||||
val pushed_at: String
|
||||
) {
|
||||
val id get() = name
|
||||
|
||||
@Transient
|
||||
val pushDate = dateFormat.parse(pushed_at)!!
|
||||
}
|
||||
repoDB.removeModules(cachedMap.keys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,10 +7,12 @@ import android.util.Base64OutputStream
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.signing.CryptoUtils.readCertificate
|
||||
import com.topjohnwu.signing.CryptoUtils.readPrivateKey
|
||||
import com.topjohnwu.signing.KeyData
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.KeyPairGenerator
|
||||
@@ -58,10 +60,10 @@ class Keygen(context: Context) : CertKeyProvider {
|
||||
|
||||
class TestProvider : CertKeyProvider {
|
||||
override val cert by lazy {
|
||||
readCertificate(javaClass.getResourceAsStream("/keys/testkey.x509.pem"))
|
||||
readCertificate(ByteArrayInputStream(KeyData.testCert()))
|
||||
}
|
||||
override val key by lazy {
|
||||
readPrivateKey(javaClass.getResourceAsStream("/keys/testkey.pk8"))
|
||||
readPrivateKey(ByteArrayInputStream(KeyData.testKey()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.core.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
@@ -40,15 +39,17 @@ object MediaStoreUtils {
|
||||
|
||||
private val relativePath get() = relativePath(Config.downloadDir)
|
||||
|
||||
@RequiresApi(api = 29)
|
||||
@RequiresApi(api = 30)
|
||||
@Throws(IOException::class)
|
||||
private fun insertFile(displayName: String): MediaStoreFile {
|
||||
val values = ContentValues()
|
||||
values.put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
|
||||
values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
|
||||
// before Android 11, MediaStore can not rename new file when file exists,
|
||||
// insert will return null. use newFile() instead.
|
||||
// When a file with the same name exists and was not created by us:
|
||||
// - Before Android 11, insert will return null
|
||||
// - On Android 11+, the system will automatically create a new name
|
||||
// Thus the reason to restrict this method call to API 30+
|
||||
val fileUri = cr.insert(tableUri, values) ?: throw IOException("Can't insert $displayName.")
|
||||
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
@@ -65,14 +66,8 @@ object MediaStoreUtils {
|
||||
throw IOException("Can't insert $displayName.")
|
||||
}
|
||||
|
||||
@RequiresApi(api = 29)
|
||||
private fun queryFile(displayName: String): UriFile? {
|
||||
if (Build.VERSION.SDK_INT < 29) {
|
||||
// Before official general purpose MediaStore API exists, fallback to file based I/O
|
||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||
parent.mkdirs()
|
||||
return LegacyUriFile(File(parent, displayName))
|
||||
}
|
||||
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA)
|
||||
// Before Android 10, we wrote the DISPLAY_NAME field when insert, so it can be used.
|
||||
val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} == ?"
|
||||
@@ -92,11 +87,17 @@ object MediaStoreUtils {
|
||||
return null
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Throws(IOException::class)
|
||||
fun getFile(displayName: String): UriFile {
|
||||
return queryFile(displayName) ?:
|
||||
/* this code path will never happen pre 29 */ insertFile(displayName)
|
||||
fun getFile(displayName: String, skipQuery: Boolean = false): UriFile {
|
||||
if (Build.VERSION.SDK_INT < 30) {
|
||||
// Fallback to file based I/O pre Android 11
|
||||
val parent = File(Environment.getExternalStorageDirectory(), relativePath)
|
||||
parent.mkdirs()
|
||||
return LegacyUriFile(File(parent, displayName))
|
||||
}
|
||||
|
||||
return if (skipQuery) insertFile(displayName)
|
||||
else queryFile(displayName) ?: insertFile(displayName)
|
||||
}
|
||||
|
||||
fun Uri.inputStream() = cr.openInputStream(this) ?: throw FileNotFoundException()
|
||||
|
@@ -1,67 +0,0 @@
|
||||
@file:JvmMultifileClass
|
||||
|
||||
package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
|
||||
interface RepoBase {
|
||||
|
||||
fun getRepos(offset: Int, limit: Int = LIMIT): List<Repo>
|
||||
fun searchRepos(query: String, offset: Int, limit: Int = LIMIT): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id AND versionCode > :versionCode LIMIT 1")
|
||||
fun getUpdatableRepoById(id: String, versionCode: Int): Repo?
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id LIMIT 1")
|
||||
fun getRepoById(id: String): Repo?
|
||||
|
||||
companion object {
|
||||
const val LIMIT = 10
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RepoByUpdatedDao : RepoBase {
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
|
||||
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM repos
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY last_update DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
|
||||
|
||||
}
|
||||
|
||||
@Dao
|
||||
interface RepoByNameDao : RepoBase {
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
|
||||
override fun getRepos(offset: Int, limit: Int): List<Repo>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM repos
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
override fun searchRepos(query: String, offset: Int, limit: Int): List<Repo>
|
||||
|
||||
|
||||
}
|
@@ -2,72 +2,89 @@ package com.topjohnwu.magisk.data.database
|
||||
|
||||
import androidx.room.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Database(version = 6, entities = [Repo::class, RepoEtag::class], exportSchema = false)
|
||||
@Database(version = 8, entities = [OnlineModule::class], exportSchema = false)
|
||||
abstract class RepoDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun repoDao() : RepoDao
|
||||
abstract fun repoByUpdatedDao(): RepoByUpdatedDao
|
||||
abstract fun repoByNameDao(): RepoByNameDao
|
||||
}
|
||||
|
||||
@Dao
|
||||
abstract class RepoDao(private val db: RepoDatabase) {
|
||||
|
||||
val repoIDList get() = getRepoID().map { it.id }
|
||||
|
||||
val repos: List<Repo> get() = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getReposNameOrder()
|
||||
else -> getReposDateOrder()
|
||||
}
|
||||
|
||||
var etagKey: String
|
||||
set(value) = addEtagRaw(RepoEtag(0, value))
|
||||
get() = etagRaw()?.key.orEmpty()
|
||||
|
||||
suspend fun clear() = withContext(Dispatchers.IO) { db.clearAllTables() }
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY last_update DESC")
|
||||
protected abstract fun getReposDateOrder(): List<Repo>
|
||||
|
||||
@Query("SELECT * FROM repos ORDER BY name COLLATE NOCASE")
|
||||
protected abstract fun getReposNameOrder(): List<Repo>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
abstract fun addRepo(repo: Repo)
|
||||
|
||||
@Query("SELECT * FROM repos WHERE id = :id")
|
||||
abstract fun getRepo(id: String): Repo?
|
||||
|
||||
@Query("SELECT id FROM repos")
|
||||
protected abstract fun getRepoID(): List<RepoID>
|
||||
abstract fun addModule(repo: OnlineModule)
|
||||
|
||||
@Delete
|
||||
abstract fun removeRepo(repo: Repo)
|
||||
abstract fun removeModule(repo: OnlineModule)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id = :id")
|
||||
abstract fun removeRepo(id: String)
|
||||
@Query("DELETE FROM modules WHERE id = :id")
|
||||
abstract fun removeModule(id: String)
|
||||
|
||||
@Query("DELETE FROM repos WHERE id IN (:idList)")
|
||||
abstract fun removeRepos(idList: Collection<String>)
|
||||
@Query("DELETE FROM modules WHERE id IN (:idList)")
|
||||
abstract fun removeModules(idList: Collection<String>)
|
||||
|
||||
@Query("SELECT * FROM etag")
|
||||
protected abstract fun etagRaw(): RepoEtag?
|
||||
@Query("SELECT * FROM modules WHERE id = :id")
|
||||
abstract fun getModule(id: String): OnlineModule?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
protected abstract fun addEtagRaw(etag: RepoEtag)
|
||||
@Query("SELECT id, last_update FROM modules")
|
||||
abstract fun getModuleStubs(): List<ModuleStub>
|
||||
|
||||
fun getModules(offset: Int, limit: Int = LIMIT) = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> getNameOrder(offset, limit)
|
||||
else -> getDateOrder(offset, limit)
|
||||
}
|
||||
|
||||
data class RepoID(
|
||||
@PrimaryKey val id: String
|
||||
)
|
||||
fun searchModules(query: String, offset: Int, limit: Int = LIMIT) = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_NAME -> searchNameOrder(query, offset, limit)
|
||||
else -> searchDateOrder(query, offset, limit)
|
||||
}
|
||||
|
||||
@Entity(tableName = "etag")
|
||||
data class RepoEtag(
|
||||
@PrimaryKey val id: Int,
|
||||
val key: String
|
||||
)
|
||||
@Query("SELECT * FROM modules WHERE id = :id AND versionCode > :versionCode LIMIT 1")
|
||||
abstract fun getUpdatableModule(id: String, versionCode: Int): OnlineModule?
|
||||
|
||||
@Query("SELECT * FROM modules ORDER BY last_update DESC LIMIT :limit OFFSET :offset")
|
||||
protected abstract fun getDateOrder(offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
@Query("SELECT * FROM modules ORDER BY name COLLATE NOCASE LIMIT :limit OFFSET :offset")
|
||||
protected abstract fun getNameOrder(offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM modules
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY last_update DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
protected abstract fun searchDateOrder(query: String, offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
@Query(
|
||||
"""SELECT *
|
||||
FROM modules
|
||||
WHERE
|
||||
(author LIKE '%' || :query || '%') ||
|
||||
(name LIKE '%' || :query || '%') ||
|
||||
(description LIKE '%' || :query || '%')
|
||||
ORDER BY name COLLATE NOCASE
|
||||
LIMIT :limit
|
||||
OFFSET :offset"""
|
||||
)
|
||||
protected abstract fun searchNameOrder(query: String, offset: Int, limit: Int): List<OnlineModule>
|
||||
|
||||
companion object {
|
||||
const val LIMIT = 10
|
||||
}
|
||||
}
|
||||
|
||||
data class ModuleStub(
|
||||
@PrimaryKey val id: String,
|
||||
val last_update: Long
|
||||
)
|
||||
|
@@ -1,20 +1,18 @@
|
||||
package com.topjohnwu.magisk.data.network
|
||||
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.model.BranchInfo
|
||||
import com.topjohnwu.magisk.core.model.RepoJson
|
||||
import com.topjohnwu.magisk.core.model.UpdateInfo
|
||||
import com.topjohnwu.magisk.core.tasks.GithubRepoInfo
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
|
||||
private const val REVISION = "revision"
|
||||
private const val MODULE = "module"
|
||||
private const val FILE = "file"
|
||||
private const val IF_NONE_MATCH = "If-None-Match"
|
||||
private const val BRANCH = "branch"
|
||||
private const val REPO = "repo"
|
||||
|
||||
private const val MAGISK_FILES = "topjohnwu/magisk_files"
|
||||
private const val MAGISK_MASTER = "topjohnwu/Magisk/master"
|
||||
private const val MAGISK_MODULES = "Magisk-Modules-Repo"
|
||||
const val MAGISK_FILES = "topjohnwu/magisk_files"
|
||||
const val MAGISK_MAIN = "topjohnwu/Magisk"
|
||||
|
||||
interface GithubPageServices {
|
||||
|
||||
@@ -34,27 +32,23 @@ interface JSDelivrServices {
|
||||
@GET("$MAGISK_FILES@{$REVISION}/bootctl")
|
||||
@Streaming
|
||||
suspend fun fetchBootctl(@Path(REVISION) revision: String = Const.BOOTCTL_REVISION): ResponseBody
|
||||
|
||||
@GET("$MAGISK_FILES@{$REVISION}/canary.json")
|
||||
suspend fun fetchCanaryUpdate(@Path(REVISION) revision: String): UpdateInfo
|
||||
|
||||
@GET("$MAGISK_MAIN@{$REVISION}/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
suspend fun fetchInstaller(@Path(REVISION) revision: String): ResponseBody
|
||||
}
|
||||
|
||||
interface GithubRawServices {
|
||||
|
||||
@GET("$MAGISK_FILES/canary/debug.json")
|
||||
suspend fun fetchCanaryUpdate(): UpdateInfo
|
||||
interface RawServices {
|
||||
|
||||
@GET
|
||||
suspend fun fetchCustomUpdate(@Url url: String): UpdateInfo
|
||||
|
||||
@GET("$MAGISK_MASTER/scripts/module_installer.sh")
|
||||
@Streaming
|
||||
suspend fun fetchInstaller(): ResponseBody
|
||||
@GET
|
||||
suspend fun fetchRepoInfo(@Url url: String): RepoJson
|
||||
|
||||
@GET("$MAGISK_MODULES/{$MODULE}/master/{$FILE}")
|
||||
suspend fun fetchModuleFile(@Path(MODULE) id: String, @Path(FILE) file: String): String
|
||||
|
||||
/**
|
||||
* This method shall be used exclusively for fetching files from urls from previous requests.
|
||||
* Him, who uses it in a wrong way, shall die in an eternal flame.
|
||||
* */
|
||||
@GET
|
||||
@Streaming
|
||||
suspend fun fetchFile(@Url url: String): ResponseBody
|
||||
@@ -66,13 +60,11 @@ interface GithubRawServices {
|
||||
|
||||
interface GithubApiServices {
|
||||
|
||||
@GET("users/${MAGISK_MODULES}/repos")
|
||||
@GET("repos/{$REPO}/branches/{$BRANCH}")
|
||||
@Headers("Accept: application/vnd.github.v3+json")
|
||||
suspend fun fetchRepos(
|
||||
@Query("page") page: Int,
|
||||
@Header(IF_NONE_MATCH) etag: String,
|
||||
@Query("sort") sort: String = "pushed",
|
||||
@Query("per_page") count: Int = 100
|
||||
): Response<List<GithubRepoInfo>>
|
||||
suspend fun fetchBranch(
|
||||
@Path(REPO, encoded = true) repo: String,
|
||||
@Path(BRANCH) branch: String
|
||||
): BranchInfo
|
||||
}
|
||||
|
||||
|
@@ -11,16 +11,14 @@ interface PreferenceModel {
|
||||
|
||||
val fileName: String
|
||||
get() = "${context.packageName}_preferences"
|
||||
val commitPrefs: Boolean
|
||||
get() = false
|
||||
|
||||
val prefs: SharedPreferences
|
||||
get() = context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
|
||||
|
||||
fun preferenceStrInt(
|
||||
name: String,
|
||||
default: Int,
|
||||
writeDefault: Boolean = false,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = object: ReadWriteProperty<PreferenceModel, Int> {
|
||||
val base = StringProperty(name, default.toString(), commit)
|
||||
override fun getValue(thisRef: PreferenceModel, property: KProperty<*>): Int =
|
||||
@@ -33,37 +31,37 @@ interface PreferenceModel {
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Boolean,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = BooleanProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Float,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = FloatProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Int,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = IntProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Long,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = LongProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: String,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = StringProperty(name, default, commit)
|
||||
|
||||
fun preference(
|
||||
name: String,
|
||||
default: Set<String>,
|
||||
commit: Boolean = commitPrefs
|
||||
commit: Boolean = false
|
||||
) = StringSetProperty(name, default, commit)
|
||||
|
||||
}
|
||||
|
@@ -6,23 +6,21 @@ import com.topjohnwu.magisk.core.Config.Value.CANARY_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.CUSTOM_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.DEFAULT_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Config.Value.STABLE_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.data.network.JSDelivrServices
|
||||
import com.topjohnwu.magisk.core.model.*
|
||||
import com.topjohnwu.magisk.data.network.*
|
||||
import retrofit2.HttpException
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
|
||||
class NetworkService(
|
||||
private val pages: GithubPageServices,
|
||||
private val raw: GithubRawServices,
|
||||
private val raw: RawServices,
|
||||
private val jsd: JSDelivrServices,
|
||||
private val api: GithubApiServices
|
||||
) {
|
||||
suspend fun fetchUpdate() = try {
|
||||
suspend fun fetchUpdate() = safe {
|
||||
var info = when (Config.updateChannel) {
|
||||
DEFAULT_CHANNEL, STABLE_CHANNEL -> fetchStableUpdate()
|
||||
BETA_CHANNEL -> fetchBetaUpdate()
|
||||
@@ -38,31 +36,62 @@ class NetworkService(
|
||||
}
|
||||
Info.remote = info
|
||||
info
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
} catch (e: HttpException) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
|
||||
// UpdateInfo
|
||||
suspend fun fetchStableUpdate() = pages.fetchStableUpdate()
|
||||
suspend fun fetchBetaUpdate() = pages.fetchBetaUpdate()
|
||||
suspend fun fetchCanaryUpdate() = raw.fetchCanaryUpdate()
|
||||
suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||
private suspend fun fetchStableUpdate() = pages.fetchStableUpdate()
|
||||
private suspend fun fetchBetaUpdate() = pages.fetchBetaUpdate()
|
||||
private suspend fun fetchCustomUpdate(url: String) = raw.fetchCustomUpdate(url)
|
||||
private suspend fun fetchCanaryUpdate(): UpdateInfo {
|
||||
val sha = fetchCanaryVersion()
|
||||
val info = jsd.fetchCanaryUpdate(sha)
|
||||
|
||||
// Byte streams
|
||||
suspend fun fetchSafetynet() = jsd.fetchSafetynet()
|
||||
suspend fun fetchBootctl() = jsd.fetchBootctl()
|
||||
suspend fun fetchInstaller() = raw.fetchInstaller()
|
||||
suspend fun fetchFile(url: String) = raw.fetchFile(url)
|
||||
fun genCDNUrl(name: String) = "${Const.Url.JS_DELIVR_URL}${MAGISK_FILES}@${sha}/${name}"
|
||||
fun ManagerJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
|
||||
fun MagiskJson.updateCopy() = copy(link = genCDNUrl(link), note = genCDNUrl(note))
|
||||
fun StubJson.updateCopy() = copy(link = genCDNUrl(link))
|
||||
fun UninstallerJson.updateCopy() = copy(link = genCDNUrl(link))
|
||||
|
||||
// Strings
|
||||
suspend fun fetchMetadata(repo: Repo) = raw.fetchModuleFile(repo.id, "module.prop")
|
||||
suspend fun fetchReadme(repo: Repo) = raw.fetchModuleFile(repo.id, "README.md")
|
||||
suspend fun fetchString(url: String) = raw.fetchString(url)
|
||||
|
||||
// API calls
|
||||
suspend fun fetchRepos(page: Int, etag: String) = api.fetchRepos(page, etag)
|
||||
return info.copy(
|
||||
app = info.app.updateCopy(),
|
||||
magisk = info.magisk.updateCopy(),
|
||||
stub = info.stub.updateCopy(),
|
||||
uninstaller = info.uninstaller.updateCopy()
|
||||
)
|
||||
}
|
||||
|
||||
private inline fun <T> safe(factory: () -> T): T? {
|
||||
return try {
|
||||
factory()
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <T> wrap(factory: () -> T): T {
|
||||
return try {
|
||||
factory()
|
||||
} catch (e: HttpException) {
|
||||
throw IOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
// Modules related
|
||||
suspend fun fetchRepoInfo(url: String = Const.Url.OFFICIAL_REPO) = safe {
|
||||
raw.fetchRepoInfo(url)
|
||||
}
|
||||
|
||||
// Fetch files
|
||||
suspend fun fetchSafetynet() = wrap { jsd.fetchSafetynet() }
|
||||
suspend fun fetchBootctl() = wrap { jsd.fetchBootctl() }
|
||||
suspend fun fetchInstaller() = wrap {
|
||||
val sha = fetchMainVersion()
|
||||
jsd.fetchInstaller(sha)
|
||||
}
|
||||
suspend fun fetchFile(url: String) = wrap { raw.fetchFile(url) }
|
||||
suspend fun fetchString(url: String) = wrap { raw.fetchString(url) }
|
||||
|
||||
private suspend fun fetchCanaryVersion() = api.fetchBranch(MAGISK_FILES, "canary").commit.sha
|
||||
private suspend fun fetchMainVersion() = api.fetchBranch(MAGISK_MAIN, "master").commit.sha
|
||||
}
|
||||
|
@@ -200,12 +200,12 @@ fun RecyclerView.setDividers(dividerVertical: Drawable?, dividerHorizontal: Draw
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("app:icon")
|
||||
@BindingAdapter("icon")
|
||||
fun Button.setIconRes(res: Int) {
|
||||
(this as MaterialButton).setIconResource(res)
|
||||
}
|
||||
|
||||
@BindingAdapter("app:icon")
|
||||
@BindingAdapter("icon")
|
||||
fun Button.setIcon(drawable: Drawable) {
|
||||
(this as MaterialButton).icon = drawable
|
||||
}
|
||||
|
@@ -17,8 +17,6 @@ val databaseModule = module {
|
||||
single { StringDao() }
|
||||
single { createRepoDatabase(get()) }
|
||||
single { get<RepoDatabase>().repoDao() }
|
||||
single { get<RepoDatabase>().repoByNameDao() }
|
||||
single { get<RepoDatabase>().repoByUpdatedDao() }
|
||||
single { createSuLogDatabase(get(Protected)).suLogDao() }
|
||||
single { RepoUpdater(get(), get()) }
|
||||
}
|
||||
|
@@ -3,13 +3,14 @@ package com.topjohnwu.magisk.di
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.data.network.GithubApiServices
|
||||
import com.topjohnwu.magisk.data.network.GithubPageServices
|
||||
import com.topjohnwu.magisk.data.network.GithubRawServices
|
||||
import com.topjohnwu.magisk.data.network.JSDelivrServices
|
||||
import com.topjohnwu.magisk.data.network.RawServices
|
||||
import com.topjohnwu.magisk.ktx.precomputedText
|
||||
import com.topjohnwu.magisk.net.Networking
|
||||
import com.topjohnwu.magisk.net.NoSSLv3SocketFactory
|
||||
@@ -31,7 +32,7 @@ import java.net.UnknownHostException
|
||||
val networkingModule = module {
|
||||
single { createOkHttpClient(get()) }
|
||||
single { createRetrofit(get()) }
|
||||
single { createApiService<GithubRawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<RawServices>(get(), Const.Url.GITHUB_RAW_URL) }
|
||||
single { createApiService<GithubApiServices>(get(), Const.Url.GITHUB_API_URL) }
|
||||
single { createApiService<GithubPageServices>(get(), Const.Url.GITHUB_PAGE_URL) }
|
||||
single { createApiService<JSDelivrServices>(get(), Const.Url.JS_DELIVR_URL) }
|
||||
@@ -40,8 +41,6 @@ val networkingModule = module {
|
||||
|
||||
private class DnsResolver(client: OkHttpClient) : Dns {
|
||||
|
||||
private var dohError = false
|
||||
private val poisonedHosts = listOf("raw.githubusercontent.com")
|
||||
private val doh by lazy {
|
||||
DnsOverHttps.Builder().client(client)
|
||||
.url(HttpUrl.get("https://cloudflare-dns.com/dns-query"))
|
||||
@@ -61,16 +60,12 @@ private class DnsResolver(client: OkHttpClient) : Dns {
|
||||
}
|
||||
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
return if (!dohError && Config.doh && poisonedHosts.contains(hostname)) {
|
||||
if (Config.doh) {
|
||||
try {
|
||||
doh.lookup(hostname)
|
||||
} catch (e: UnknownHostException) {
|
||||
dohError = true
|
||||
Dns.SYSTEM.lookup(hostname)
|
||||
}
|
||||
} else {
|
||||
Dns.SYSTEM.lookup(hostname)
|
||||
return doh.lookup(hostname)
|
||||
} catch (e: UnknownHostException) {}
|
||||
}
|
||||
return Dns.SYSTEM.lookup(hostname)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +73,11 @@ private class DnsResolver(client: OkHttpClient) : Dns {
|
||||
fun createOkHttpClient(context: Context): OkHttpClient {
|
||||
val builder = OkHttpClient.Builder()
|
||||
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
if (BuildConfig.DEBUG) {
|
||||
builder.addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
})
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
|
||||
if (!Networking.init(context)) {
|
||||
Info.hasGMS = false
|
||||
|
@@ -20,7 +20,7 @@ val viewModelModules = module {
|
||||
viewModel { HideViewModel() }
|
||||
viewModel { HomeViewModel(get()) }
|
||||
viewModel { LogViewModel(get()) }
|
||||
viewModel { ModuleViewModel(get(), get(), get()) }
|
||||
viewModel { ModuleViewModel(get(), get()) }
|
||||
viewModel { SafetynetViewModel() }
|
||||
viewModel { SettingsViewModel(get()) }
|
||||
viewModel { SuperuserViewModel(get(), get()) }
|
||||
@@ -29,6 +29,6 @@ val viewModelModules = module {
|
||||
viewModel { MainViewModel() }
|
||||
|
||||
// Legacy
|
||||
viewModel { (args: FlashFragmentArgs) -> FlashViewModel(args, get()) }
|
||||
viewModel { (args: FlashFragmentArgs) -> FlashViewModel(args) }
|
||||
viewModel { SuRequestViewModel(get(), get(), get(SUTimeout), get()) }
|
||||
}
|
||||
|
@@ -1,8 +1,11 @@
|
||||
package com.topjohnwu.magisk.events
|
||||
|
||||
import android.os.Build
|
||||
import android.os.PowerManager
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.MenuItem
|
||||
import android.widget.PopupMenu
|
||||
import androidx.core.content.getSystemService
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@@ -13,6 +16,7 @@ object RebootEvent {
|
||||
private fun reboot(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_reboot_normal -> systemReboot()
|
||||
R.id.action_reboot_userspace -> systemReboot("userspace")
|
||||
R.id.action_reboot_bootloader -> systemReboot("bootloader")
|
||||
R.id.action_reboot_download -> systemReboot("download")
|
||||
R.id.action_reboot_edl -> systemReboot("edl")
|
||||
@@ -25,6 +29,9 @@ object RebootEvent {
|
||||
fun inflateMenu(activity: BaseActivity): PopupMenu {
|
||||
val themeWrapper = ContextThemeWrapper(activity, R.style.Foundation_PopupMenu)
|
||||
val menu = PopupMenu(themeWrapper, activity.findViewById(R.id.action_reboot))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
|
||||
activity.getSystemService<PowerManager>()?.isRebootingUserspaceSupported == true)
|
||||
menu.menu.getItem(R.id.action_reboot_userspace).isVisible = true
|
||||
activity.menuInflater.inflate(R.menu.menu_reboot, menu.menu)
|
||||
menu.setOnMenuItemClickListener(::reboot)
|
||||
return menu
|
||||
|
@@ -35,11 +35,9 @@ class SnackbarEvent private constructor(
|
||||
) = Snackbar.make(view, message, length).apply(builder).show()
|
||||
|
||||
override fun invoke(activity: BaseUIActivity<*, *>) {
|
||||
if (activity is BaseUIActivity<*, *>) {
|
||||
snackbar(activity.snackbarView,
|
||||
msg.getText(activity.resources).toString(),
|
||||
length, builder)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import android.app.Activity
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.navigation.NavDirections
|
||||
import com.topjohnwu.magisk.MainDirections
|
||||
@@ -12,7 +13,7 @@ import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.base.ActivityResultCallback
|
||||
import com.topjohnwu.magisk.core.base.BaseActivity
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MarkDownWindow
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
@@ -22,10 +23,10 @@ class ViewActionEvent(val action: BaseActivity.() -> Unit) : ViewEvent(), Activi
|
||||
override fun invoke(activity: BaseUIActivity<*, *>) = action(activity)
|
||||
}
|
||||
|
||||
class OpenChangelogEvent(val item: Repo) : ViewEventWithScope(), ContextExecutor {
|
||||
class OpenReadmeEvent(val item: OnlineModule) : ViewEventWithScope(), ContextExecutor {
|
||||
override fun invoke(context: Context) {
|
||||
scope.launch {
|
||||
MarkDownWindow.show(context, null, item::readme)
|
||||
MarkDownWindow.show(context, null, item::notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,9 +59,11 @@ class DieEvent : ViewEvent(), ActivityExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
class ShowUIEvent : ViewEvent(), ActivityExecutor {
|
||||
class ShowUIEvent(private val delegate: View.AccessibilityDelegate?)
|
||||
: ViewEvent(), ActivityExecutor {
|
||||
override fun invoke(activity: BaseUIActivity<*, *>) {
|
||||
activity.setContentView()
|
||||
activity.setAccessibilityDelegate(delegate)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -2,7 +2,6 @@ package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Action
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.ktx.res
|
||||
@@ -16,7 +15,7 @@ class ManagerInstallDialog : DialogEvent() {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
with(dialog) {
|
||||
val subject = Subject.Manager(Action.APK.Upgrade)
|
||||
val subject = Subject.Manager()
|
||||
|
||||
applyTitle(R.string.repo_install_title.res(R.string.app_name.res()))
|
||||
applyMessage(R.string.repo_install_msg.res(subject.title))
|
||||
|
@@ -5,10 +5,10 @@ import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Action
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
|
||||
class ModuleInstallDialog(private val item: Repo) : DialogEvent() {
|
||||
class ModuleInstallDialog(private val item: OnlineModule) : DialogEvent() {
|
||||
|
||||
override fun build(dialog: MagiskDialog) {
|
||||
with(dialog) {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package com.topjohnwu.magisk.events.dialog
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.widget.Toast
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
@@ -17,8 +18,7 @@ class UninstallDialog : DialogEvent() {
|
||||
.applyMessage(R.string.uninstall_magisk_msg)
|
||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
titleRes = R.string.restore_img
|
||||
preventDismiss = true
|
||||
onClick { restore(dialog) }
|
||||
onClick { restore() }
|
||||
}
|
||||
if (Info.remote.uninstaller.link.isNotEmpty()) {
|
||||
dialog.applyButton(MagiskDialog.ButtonType.NEGATIVE) {
|
||||
@@ -28,13 +28,12 @@ class UninstallDialog : DialogEvent() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun restore(dialog: MagiskDialog) {
|
||||
dialog.applyTitle(R.string.restore_img)
|
||||
.applyMessage(R.string.restore_img_msg)
|
||||
.applyButton(MagiskDialog.ButtonType.POSITIVE) {
|
||||
title = ""
|
||||
@Suppress("DEPRECATION")
|
||||
private fun restore() {
|
||||
val dialog = ProgressDialog(dialog.context).apply {
|
||||
setMessage(dialog.context.getString(R.string.restore_img_msg))
|
||||
show()
|
||||
}
|
||||
.cancellable(false)
|
||||
|
||||
Shell.su("restore_imgs").submit { result ->
|
||||
dialog.dismiss()
|
||||
|
193
app/src/main/java/com/topjohnwu/magisk/ktx/RecyclerView.kt
Normal file
193
app/src/main/java/com/topjohnwu/magisk/ktx/RecyclerView.kt
Normal file
@@ -0,0 +1,193 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.topjohnwu.magisk.ktx
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.widget.EdgeEffect
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
fun RecyclerView.addInvalidateItemDecorationsObserver() {
|
||||
|
||||
adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
invalidateItemDecorations()
|
||||
}
|
||||
|
||||
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
|
||||
invalidateItemDecorations()
|
||||
}
|
||||
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
invalidateItemDecorations()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun RecyclerView.addVerticalPadding(paddingTop: Int = 0, paddingBottom: Int = 0) {
|
||||
addItemDecoration(VerticalPaddingDecoration(paddingTop, paddingBottom))
|
||||
}
|
||||
|
||||
private class VerticalPaddingDecoration(private val paddingTop: Int = 0, private val paddingBottom: Int = 0) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private var allowTop: Boolean = true
|
||||
private var allowBottom: Boolean = true
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
val adapter = parent.adapter ?: return
|
||||
val position = parent.getChildAdapterPosition(view)
|
||||
val count = adapter.itemCount
|
||||
if (position == 0 && allowTop) {
|
||||
outRect.top = paddingTop
|
||||
} else if (position == count - 1 && allowBottom) {
|
||||
outRect.bottom = paddingBottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.addSimpleItemDecoration(
|
||||
left: Int = 0,
|
||||
top: Int = 0,
|
||||
right: Int = 0,
|
||||
bottom: Int = 0,
|
||||
) {
|
||||
addItemDecoration(SimpleItemDecoration(left, top, right, bottom))
|
||||
}
|
||||
|
||||
private class SimpleItemDecoration(
|
||||
private val left: Int = 0,
|
||||
private val top: Int = 0,
|
||||
private val right: Int = 0,
|
||||
private val bottom: Int = 0
|
||||
) : RecyclerView.ItemDecoration() {
|
||||
|
||||
private var allowLeft: Boolean = true
|
||||
private var allowTop: Boolean = true
|
||||
private var allowRight: Boolean = true
|
||||
private var allowBottom: Boolean = true
|
||||
|
||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
||||
if (parent.adapter == null) {
|
||||
return
|
||||
}
|
||||
if (allowLeft) {
|
||||
outRect.left = left
|
||||
}
|
||||
if (allowTop) {
|
||||
outRect.top = top
|
||||
}
|
||||
if (allowRight) {
|
||||
outRect.right = right
|
||||
}
|
||||
if (allowBottom) {
|
||||
outRect.top = bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun RecyclerView.fixEdgeEffect(overScrollIfContentScrolls: Boolean = true, alwaysClipToPadding: Boolean = true) {
|
||||
if (overScrollIfContentScrolls) {
|
||||
val listener = OverScrollIfContentScrollsListener()
|
||||
addOnLayoutChangeListener(listener)
|
||||
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, listener)
|
||||
} else {
|
||||
val listener = getTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener) as? OverScrollIfContentScrollsListener
|
||||
if (listener != null) {
|
||||
removeOnLayoutChangeListener(listener)
|
||||
setTag(R.id.tag_rikka_recyclerView_OverScrollIfContentScrollsListener, null)
|
||||
}
|
||||
}
|
||||
|
||||
edgeEffectFactory = if (alwaysClipToPadding && !clipToPadding) {
|
||||
AlwaysClipToPaddingEdgeEffectFactory()
|
||||
} else {
|
||||
RecyclerView.EdgeEffectFactory()
|
||||
}
|
||||
}
|
||||
|
||||
private class OverScrollIfContentScrollsListener : View.OnLayoutChangeListener {
|
||||
private var show = true
|
||||
override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||
if (shouldDrawOverScroll(v as RecyclerView) != show) {
|
||||
show = !show
|
||||
if (show) {
|
||||
v.setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS)
|
||||
} else {
|
||||
v.setOverScrollMode(View.OVER_SCROLL_NEVER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shouldDrawOverScroll(recyclerView: RecyclerView): Boolean {
|
||||
if (recyclerView.layoutManager == null || recyclerView.adapter == null || recyclerView.adapter!!.itemCount == 0) {
|
||||
return false
|
||||
}
|
||||
if (recyclerView.layoutManager is LinearLayoutManager) {
|
||||
val itemCount = recyclerView.layoutManager!!.itemCount
|
||||
val firstPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findFirstCompletelyVisibleItemPosition()
|
||||
val lastPosition: Int = (recyclerView.layoutManager as LinearLayoutManager?)!!.findLastCompletelyVisibleItemPosition()
|
||||
return firstPosition != 0 || lastPosition != itemCount - 1
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private class AlwaysClipToPaddingEdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
|
||||
|
||||
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {
|
||||
|
||||
return object : EdgeEffect(view.context) {
|
||||
private var ensureSize = false
|
||||
|
||||
private fun ensureSize() {
|
||||
if (ensureSize) return
|
||||
ensureSize = true
|
||||
|
||||
when (direction) {
|
||||
DIRECTION_LEFT -> {
|
||||
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
|
||||
view.measuredWidth - view.paddingLeft - view.paddingRight)
|
||||
}
|
||||
DIRECTION_TOP -> {
|
||||
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
|
||||
view.measuredHeight - view.paddingTop - view.paddingBottom)
|
||||
}
|
||||
DIRECTION_RIGHT -> {
|
||||
setSize(view.measuredHeight - view.paddingTop - view.paddingBottom,
|
||||
view.measuredWidth - view.paddingLeft - view.paddingRight)
|
||||
}
|
||||
DIRECTION_BOTTOM -> {
|
||||
setSize(view.measuredWidth - view.paddingLeft - view.paddingRight,
|
||||
view.measuredHeight - view.paddingTop - view.paddingBottom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun draw(c: Canvas): Boolean {
|
||||
ensureSize()
|
||||
|
||||
val restore = c.save()
|
||||
when (direction) {
|
||||
DIRECTION_LEFT -> {
|
||||
c.translate(view.paddingBottom.toFloat(), 0f)
|
||||
}
|
||||
DIRECTION_TOP -> {
|
||||
c.translate(view.paddingLeft.toFloat(), view.paddingTop.toFloat())
|
||||
}
|
||||
DIRECTION_RIGHT -> {
|
||||
c.translate(-view.paddingTop.toFloat(), 0f)
|
||||
}
|
||||
DIRECTION_BOTTOM -> {
|
||||
c.translate(view.paddingRight.toFloat(), view.paddingBottom.toFloat())
|
||||
}
|
||||
}
|
||||
val res = super.draw(c)
|
||||
c.restoreToCount(restore)
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,15 +1,17 @@
|
||||
package com.topjohnwu.magisk.ktx
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import android.content.Intent
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.content.pm.ServiceInfo.FLAG_ISOLATED_PROCESS
|
||||
import android.content.pm.ServiceInfo.FLAG_USE_APP_ZYGOTE
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.database.Cursor
|
||||
@@ -57,30 +59,10 @@ import java.lang.reflect.Array as JArray
|
||||
|
||||
val packageName: String get() = get<Context>().packageName
|
||||
|
||||
val PackageInfo.processes
|
||||
get() = activities?.processNames.orEmpty() +
|
||||
services?.processNames.orEmpty() +
|
||||
receivers?.processNames.orEmpty() +
|
||||
providers?.processNames.orEmpty()
|
||||
val ServiceInfo.isIsolated get() = (flags and FLAG_ISOLATED_PROCESS) != 0
|
||||
|
||||
val Array<out ComponentInfo>.processNames get() = mapNotNull { it.processName }
|
||||
|
||||
val ApplicationInfo.packageInfo: PackageInfo get() {
|
||||
val pm = get<PackageManager>()
|
||||
|
||||
return try {
|
||||
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||
pm.getPackageInfo(packageName, request)
|
||||
} catch (e: Exception) {
|
||||
// Exceed binder data transfer limit, fetch each component type separately
|
||||
pm.getPackageInfo(packageName, 0).apply {
|
||||
runCatching { activities = pm.getPackageInfo(packageName, GET_ACTIVITIES).activities }
|
||||
runCatching { services = pm.getPackageInfo(packageName, GET_SERVICES).services }
|
||||
runCatching { receivers = pm.getPackageInfo(packageName, GET_RECEIVERS).receivers }
|
||||
runCatching { providers = pm.getPackageInfo(packageName, GET_PROVIDERS).providers }
|
||||
}
|
||||
}
|
||||
}
|
||||
@get:SuppressLint("InlinedApi")
|
||||
val ServiceInfo.useAppZygote get() = (flags and FLAG_USE_APP_ZYGOTE) != 0
|
||||
|
||||
fun Context.rawResource(id: Int) = resources.openRawResource(id)
|
||||
|
||||
|
@@ -10,7 +10,6 @@ import android.view.WindowManager
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.view.forEach
|
||||
import androidx.core.view.setPadding
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.navigation.NavDirections
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
@@ -26,9 +25,9 @@ import com.topjohnwu.magisk.ui.home.HomeFragmentDirections
|
||||
import com.topjohnwu.magisk.utils.HideBottomViewOnScrollBehavior
|
||||
import com.topjohnwu.magisk.utils.HideTopViewOnScrollBehavior
|
||||
import com.topjohnwu.magisk.utils.HideableBehavior
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.view.MagiskDialog
|
||||
import com.topjohnwu.magisk.view.Shortcuts
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class MainViewModel : BaseViewModel()
|
||||
@@ -39,14 +38,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
||||
override val viewModel by viewModel<MainViewModel>()
|
||||
override val navHost: Int = R.id.main_nav_host
|
||||
|
||||
//This temporarily fixes unwanted feature of BottomNavigationView - where the view applies
|
||||
//padding on itself given insets are not consumed beforehand. Unfortunately the listener
|
||||
//implementation doesn't favor us against the design library, so on re-create it's often given
|
||||
//upper hand.
|
||||
private val navObserver = ViewTreeObserver.OnGlobalLayoutListener {
|
||||
binding.mainNavigation.setPadding(0)
|
||||
}
|
||||
|
||||
private var isRootFragment = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
@@ -100,8 +91,6 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
||||
(currentFragment as? ReselectionTarget)?.onReselected()
|
||||
}
|
||||
|
||||
binding.mainNavigation.viewTreeObserver.addOnGlobalLayoutListener(navObserver)
|
||||
|
||||
val section = if (intent.action == ACTION_APPLICATION_PREFERENCES) Const.Nav.SETTINGS
|
||||
else intent.getStringExtra(Const.Key.OPEN_SECTION)
|
||||
getScreen(section)?.navigate()
|
||||
@@ -116,16 +105,11 @@ open class MainActivity : BaseUIActivity<MainViewModel, ActivityMainMd2Binding>(
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
binding.mainNavigation.menu.apply {
|
||||
findItem(R.id.superuserFragment)?.isEnabled = Info.env.isActive
|
||||
findItem(R.id.superuserFragment)?.isEnabled = Utils.showSuperUser()
|
||||
findItem(R.id.logFragment)?.isEnabled = Info.env.isActive
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
binding.mainNavigation.viewTreeObserver.removeOnGlobalLayoutListener(navObserver)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> onBackPressed()
|
||||
|
@@ -32,6 +32,10 @@ class FlashFragment : BaseUIFragment<FlashViewModel, FragmentFlashMd2Binding>()
|
||||
super.onStart()
|
||||
setHasOptionsMenu(true)
|
||||
activity.setTitle(R.string.flash_screen_title)
|
||||
|
||||
viewModel.subtitle.observe(this) {
|
||||
activity.supportActionBar?.setSubtitle(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package com.topjohnwu.magisk.ui.flash
|
||||
|
||||
import android.content.res.Resources
|
||||
import android.net.Uri
|
||||
import android.view.MenuItem
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
@@ -13,31 +14,28 @@ import com.topjohnwu.magisk.arch.itemBindingOf
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.tasks.FlashZip
|
||||
import com.topjohnwu.magisk.core.tasks.MagiskInstaller
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.databinding.RvBindingAdapter
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.*
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
import com.topjohnwu.magisk.view.Notifications
|
||||
import com.topjohnwu.superuser.CallbackList
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FlashViewModel(
|
||||
args: FlashFragmentArgs,
|
||||
private val resources: Resources
|
||||
args: FlashFragmentArgs
|
||||
) : BaseViewModel() {
|
||||
|
||||
@get:Bindable
|
||||
var showReboot = Shell.rootAccess()
|
||||
set(value) = set(value, field, { field = it }, BR.showReboot)
|
||||
|
||||
@get:Bindable
|
||||
var behaviorText = resources.getString(R.string.flashing)
|
||||
set(value) = set(value, field, { field = it }, BR.behaviorText)
|
||||
private val _subtitle = MutableLiveData(R.string.flashing)
|
||||
val subtitle get() = _subtitle as LiveData<Int>
|
||||
|
||||
val adapter = RvBindingAdapter<ConsoleItem>()
|
||||
val items = diffListOf<ConsoleItem>()
|
||||
@@ -92,9 +90,9 @@ class FlashViewModel(
|
||||
|
||||
private fun onResult(success: Boolean) {
|
||||
state = if (success) State.LOADED else State.LOADING_FAILED
|
||||
behaviorText = when {
|
||||
success -> resources.getString(R.string.done)
|
||||
else -> resources.getString(R.string.failure)
|
||||
when {
|
||||
success -> _subtitle.postValue(R.string.done)
|
||||
else -> _subtitle.postValue(R.string.failure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,10 +104,9 @@ class FlashViewModel(
|
||||
}
|
||||
|
||||
private fun savePressed() = withExternalRW {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val name = Const.MAGISK_INSTALL_LOG_FILENAME.format(now.toTime(timeFormatStandard))
|
||||
val file = MediaStoreUtils.getFile(name)
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val name = "magisk_install_log_%s.log".format(now.toTime(timeFormatStandard))
|
||||
val file = MediaStoreUtils.getFile(name, true)
|
||||
file.uri.outputStream().bufferedWriter().use { writer ->
|
||||
logItems.forEach {
|
||||
writer.write(it)
|
||||
@@ -119,7 +116,6 @@ class FlashViewModel(
|
||||
SnackbarEvent(file.toString()).publish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun restartPressed() = reboot()
|
||||
}
|
||||
|
@@ -1,47 +0,0 @@
|
||||
package com.topjohnwu.magisk.ui.hide
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
|
||||
class HideTarget(line: String) {
|
||||
val packageName: String
|
||||
val process: String
|
||||
|
||||
init {
|
||||
val split = line.split(Regex("\\|"), 2)
|
||||
packageName = split[0]
|
||||
process = split.getOrElse(1) { packageName }
|
||||
}
|
||||
}
|
||||
|
||||
class HideAppInfo(info: ApplicationInfo, pm: PackageManager)
|
||||
: ApplicationInfo(info), Comparable<HideAppInfo> {
|
||||
|
||||
val label = info.getLabel(pm)
|
||||
val iconImage: Drawable = info.loadIcon(pm)
|
||||
|
||||
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<HideAppInfo>(
|
||||
{ it.label.toLowerCase(currentLocale) },
|
||||
{ it.packageName }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class HideProcessInfo(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
val isHidden: Boolean
|
||||
)
|
||||
|
||||
class HideAppTarget(
|
||||
val info: HideAppInfo,
|
||||
val processes: List<HideProcessInfo>
|
||||
) : Comparable<HideAppTarget> {
|
||||
override fun compareTo(other: HideAppTarget) = compareValuesBy(this, other) { it.info }
|
||||
}
|
@@ -12,6 +12,9 @@ import androidx.recyclerview.widget.RecyclerView
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.databinding.FragmentHideMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -49,6 +52,21 @@ class HideFragment : BaseUIFragment<HideViewModel, FragmentHideMd2Binding>() {
|
||||
}
|
||||
})
|
||||
|
||||
val resource = requireContext().resources
|
||||
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||
binding.hideContent.addVerticalPadding(
|
||||
l_50,
|
||||
l1 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
|
||||
)
|
||||
binding.hideContent.addSimpleItemDecoration(
|
||||
left = l1,
|
||||
top = l_50,
|
||||
right = l1,
|
||||
bottom = l_50,
|
||||
)
|
||||
binding.hideContent.fixEdgeEffect()
|
||||
|
||||
val lama = binding.hideContent.layoutManager ?: return
|
||||
lama.isAutoMeasureEnabled = false
|
||||
}
|
||||
|
105
app/src/main/java/com/topjohnwu/magisk/ui/hide/HideInfo.kt
Normal file
105
app/src/main/java/com/topjohnwu/magisk/ui/hide/HideInfo.kt
Normal file
@@ -0,0 +1,105 @@
|
||||
package com.topjohnwu.magisk.ui.hide
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.ComponentInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.*
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import com.topjohnwu.magisk.core.utils.currentLocale
|
||||
import com.topjohnwu.magisk.ktx.getLabel
|
||||
import com.topjohnwu.magisk.ktx.isIsolated
|
||||
import com.topjohnwu.magisk.ktx.useAppZygote
|
||||
|
||||
class CmdlineHiddenItem(line: String) {
|
||||
val packageName: String
|
||||
val process: String
|
||||
|
||||
init {
|
||||
val split = line.split(Regex("\\|"), 2)
|
||||
packageName = split[0]
|
||||
process = split.getOrElse(1) { packageName }
|
||||
}
|
||||
}
|
||||
|
||||
const val ISOLATED_MAGIC = "isolated"
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
class HideAppInfo(info: ApplicationInfo, pm: PackageManager, hideList: List<CmdlineHiddenItem>)
|
||||
: ApplicationInfo(info), Comparable<HideAppInfo> {
|
||||
|
||||
val label = info.getLabel(pm)
|
||||
val iconImage: Drawable = info.loadIcon(pm)
|
||||
val processes = fetchProcesses(pm, hideList)
|
||||
|
||||
override fun compareTo(other: HideAppInfo) = comparator.compare(this, other)
|
||||
|
||||
private fun fetchProcesses(
|
||||
pm: PackageManager,
|
||||
hideList: List<CmdlineHiddenItem>
|
||||
): List<HideProcessInfo> {
|
||||
// Fetch full PackageInfo
|
||||
val baseFlag = MATCH_DISABLED_COMPONENTS or MATCH_UNINSTALLED_PACKAGES
|
||||
val packageInfo = try {
|
||||
val request = GET_ACTIVITIES or GET_SERVICES or GET_RECEIVERS or GET_PROVIDERS
|
||||
pm.getPackageInfo(packageName, baseFlag or request)
|
||||
} catch (e: NameNotFoundException) {
|
||||
// EdXposed hooked, issue#3276
|
||||
return emptyList()
|
||||
} catch (e: Exception) {
|
||||
// Exceed binder data transfer limit, fetch each component type separately
|
||||
pm.getPackageInfo(packageName, baseFlag).apply {
|
||||
runCatching { activities = pm.getPackageInfo(packageName, baseFlag or GET_ACTIVITIES).activities }
|
||||
runCatching { services = pm.getPackageInfo(packageName, baseFlag or GET_SERVICES).services }
|
||||
runCatching { receivers = pm.getPackageInfo(packageName, baseFlag or GET_RECEIVERS).receivers }
|
||||
runCatching { providers = pm.getPackageInfo(packageName, baseFlag or GET_PROVIDERS).providers }
|
||||
}
|
||||
}
|
||||
|
||||
val hidden = hideList.filter { it.packageName == packageName || it.packageName == ISOLATED_MAGIC }
|
||||
fun createProcess(name: String, pkg: String = packageName): HideProcessInfo {
|
||||
return HideProcessInfo(name, pkg, hidden.any { it.process == name })
|
||||
}
|
||||
|
||||
var haveAppZygote = false
|
||||
fun Array<out ComponentInfo>.processes() = map { createProcess(it.processName) }
|
||||
fun Array<ServiceInfo>.processes() = map {
|
||||
if (it.isIsolated) {
|
||||
if (it.useAppZygote) {
|
||||
haveAppZygote = true
|
||||
// Using app zygote, don't need to track the process
|
||||
null
|
||||
} else {
|
||||
createProcess("${it.processName}:${it.name}", ISOLATED_MAGIC)
|
||||
}
|
||||
} else {
|
||||
createProcess(it.processName)
|
||||
}
|
||||
}
|
||||
|
||||
return with(packageInfo) {
|
||||
activities?.processes().orEmpty() +
|
||||
services?.processes().orEmpty() +
|
||||
receivers?.processes().orEmpty() +
|
||||
providers?.processes().orEmpty() +
|
||||
listOf(if (haveAppZygote) createProcess("${processName}_zygote") else null)
|
||||
}.filterNotNull().distinctBy { it.name }.sortedBy { it.name }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<HideAppInfo>(
|
||||
{ it.label.toLowerCase(currentLocale) },
|
||||
{ it.packageName }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class HideProcessInfo(
|
||||
val name: String,
|
||||
val packageName: String,
|
||||
var isHidden: Boolean
|
||||
) {
|
||||
val isIsolated get() = name == ISOLATED_MAGIC
|
||||
val isAppZygote get() = name.endsWith("_zygote")
|
||||
}
|
@@ -12,14 +12,13 @@ import com.topjohnwu.magisk.utils.set
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class HideItem(
|
||||
app: HideAppTarget
|
||||
) : ObservableItem<HideItem>(), Comparable<HideItem> {
|
||||
class HideRvItem(
|
||||
val info: HideAppInfo
|
||||
) : ObservableItem<HideRvItem>(), Comparable<HideRvItem> {
|
||||
|
||||
override val layoutRes = R.layout.item_hide_md2
|
||||
override val layoutRes get() = R.layout.item_hide_md2
|
||||
|
||||
val info = app.info
|
||||
val processes = app.processes.map { HideProcessItem(it) }
|
||||
val processes = info.processes.map { HideProcessRvItem(it) }
|
||||
|
||||
@get:Bindable
|
||||
var isExpanded = false
|
||||
@@ -39,14 +38,18 @@ class HideItem(
|
||||
get() = state
|
||||
set(value) = set(value, state, { state = it }, BR.hiddenState) {
|
||||
if (value == true) {
|
||||
processes.filterNot { it.isHidden }
|
||||
processes
|
||||
.filterNot { it.isHidden }
|
||||
.filter { isExpanded || it.defaultSelection }
|
||||
} else {
|
||||
processes
|
||||
.filter { it.isHidden }
|
||||
}.forEach { it.toggle() }
|
||||
}
|
||||
|
||||
init {
|
||||
processes.forEach { it.addOnPropertyChangedCallback(BR.hidden) { recalculateChecked() } }
|
||||
addOnPropertyChangedCallback(BR.expanded) { recalculateChecked() }
|
||||
recalculateChecked()
|
||||
}
|
||||
|
||||
@@ -57,17 +60,26 @@ class HideItem(
|
||||
|
||||
private fun recalculateChecked() {
|
||||
itemsChecked = processes.count { it.isHidden }
|
||||
state = when (itemsChecked) {
|
||||
state = if (isExpanded) {
|
||||
when (itemsChecked) {
|
||||
0 -> false
|
||||
processes.size -> true
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
val defaultProcesses = processes.filter { it.defaultSelection }
|
||||
when (defaultProcesses.count { it.isHidden }) {
|
||||
0 -> false
|
||||
defaultProcesses.size -> true
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun compareTo(other: HideItem) = comparator.compare(this, other)
|
||||
override fun compareTo(other: HideRvItem) = comparator.compare(this, other)
|
||||
|
||||
companion object {
|
||||
private val comparator = compareBy<HideItem>(
|
||||
private val comparator = compareBy<HideRvItem>(
|
||||
{ it.itemsChecked == 0 },
|
||||
{ it.info }
|
||||
)
|
||||
@@ -75,16 +87,17 @@ class HideItem(
|
||||
|
||||
}
|
||||
|
||||
class HideProcessItem(
|
||||
class HideProcessRvItem(
|
||||
val process: HideProcessInfo
|
||||
) : ObservableItem<HideProcessItem>() {
|
||||
) : ObservableItem<HideProcessRvItem>() {
|
||||
|
||||
override val layoutRes = R.layout.item_hide_process_md2
|
||||
override val layoutRes get() = R.layout.item_hide_process_md2
|
||||
|
||||
@get:Bindable
|
||||
var isHidden = process.isHidden
|
||||
set(value) = set(value, field, { field = it }, BR.hidden) {
|
||||
val arg = if (isHidden) "add" else "rm"
|
||||
var isHidden
|
||||
get() = process.isHidden
|
||||
set(value) = set(value, process.isHidden, { process.isHidden = it }, BR.hidden) {
|
||||
val arg = if (it) "add" else "rm"
|
||||
val (name, pkg) = process
|
||||
Shell.su("magiskhide --$arg $pkg $name").submit()
|
||||
}
|
||||
@@ -93,7 +106,10 @@ class HideProcessItem(
|
||||
isHidden = !isHidden
|
||||
}
|
||||
|
||||
override fun contentSameAs(other: HideProcessItem) = process == other.process
|
||||
override fun itemSameAs(other: HideProcessItem) = process.name == other.process.name
|
||||
val defaultSelection get() =
|
||||
process.isIsolated || process.isAppZygote || process.name == process.packageName
|
||||
|
||||
override fun contentSameAs(other: HideProcessRvItem) = process == other.process
|
||||
override fun itemSameAs(other: HideProcessRvItem) = process.name == other.process.name
|
||||
|
||||
}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
package com.topjohnwu.magisk.ui.hide
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES
|
||||
import android.os.Process
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
@@ -11,9 +14,7 @@ import com.topjohnwu.magisk.arch.filterableListOf
|
||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.ktx.packageInfo
|
||||
import com.topjohnwu.magisk.ktx.packageName
|
||||
import com.topjohnwu.magisk.ktx.processes
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
import com.topjohnwu.superuser.Shell
|
||||
@@ -32,20 +33,27 @@ class HideViewModel : BaseViewModel(), Queryable {
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var isShowOS = false
|
||||
set(value) = set(value, field, { field = it }, BR.showOS) {
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
@get:Bindable
|
||||
var query = ""
|
||||
set(value) = set(value, field, { field = it }, BR.query) {
|
||||
submitQuery()
|
||||
}
|
||||
|
||||
val items = filterableListOf<HideItem>()
|
||||
val itemBinding = itemBindingOf<HideItem> {
|
||||
val items = filterableListOf<HideRvItem>()
|
||||
val itemBinding = itemBindingOf<HideRvItem> {
|
||||
it.bindExtra(BR.viewModel, this)
|
||||
}
|
||||
val itemInternalBinding = itemBindingOf<HideProcessItem> {
|
||||
val itemInternalBinding = itemBindingOf<HideProcessRvItem> {
|
||||
it.bindExtra(BR.viewModel, this)
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
if (!Utils.showSuperUser()) {
|
||||
state = State.LOADING_FAILED
|
||||
@@ -54,14 +62,13 @@ class HideViewModel : BaseViewModel(), Queryable {
|
||||
state = State.LOADING
|
||||
val (apps, diff) = withContext(Dispatchers.Default) {
|
||||
val pm = get<PackageManager>()
|
||||
val hides = Shell.su("magiskhide --ls").exec().out.map { HideTarget(it) }
|
||||
val apps = pm.getInstalledApplications(0)
|
||||
val hideList = Shell.su("magiskhide --ls").exec().out.map { CmdlineHiddenItem(it) }
|
||||
val apps = pm.getInstalledApplications(MATCH_UNINSTALLED_PACKAGES)
|
||||
.asSequence()
|
||||
.filter { it.enabled && it.uid >= 10000 && !blacklist.contains(it.packageName) }
|
||||
.map { HideAppInfo(it, pm) }
|
||||
.map { createTarget(it, hides) }
|
||||
.filter { it.enabled && !blacklist.contains(it.packageName) }
|
||||
.map { HideAppInfo(it, pm, hideList) }
|
||||
.filter { it.processes.isNotEmpty() }
|
||||
.map { HideItem(it) }
|
||||
.map { HideRvItem(it) }
|
||||
.toList()
|
||||
.sorted()
|
||||
apps to items.calculateDiff(apps)
|
||||
@@ -72,24 +79,18 @@ class HideViewModel : BaseViewModel(), Queryable {
|
||||
|
||||
// ---
|
||||
|
||||
private fun createTarget(info: HideAppInfo, hideList: List<HideTarget>): HideAppTarget {
|
||||
val pkg = info.packageName
|
||||
val hidden = hideList.filter { it.packageName == pkg }
|
||||
val processNames = info.packageInfo.processes.distinct()
|
||||
val processes = processNames.map { name ->
|
||||
HideProcessInfo(name, pkg, hidden.any { name == it.process })
|
||||
}
|
||||
return HideAppTarget(info, processes)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
override fun query() {
|
||||
items.filter {
|
||||
fun showHidden() = it.itemsChecked != 0
|
||||
|
||||
fun filterSystem() =
|
||||
isShowSystem || it.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
|
||||
fun filterSystem() = isShowSystem || it.info.flags and ApplicationInfo.FLAG_SYSTEM == 0
|
||||
|
||||
fun isApp(uid: Int) = run {
|
||||
val appId: Int = uid % 100000
|
||||
appId >= Process.FIRST_APPLICATION_UID && appId <= Process.LAST_APPLICATION_UID
|
||||
}
|
||||
|
||||
fun filterOS() = (isShowSystem && isShowOS) || isApp(it.info.uid)
|
||||
|
||||
fun filterQuery(): Boolean {
|
||||
fun inName() = it.info.label.contains(query, true)
|
||||
@@ -98,7 +99,7 @@ class HideViewModel : BaseViewModel(), Queryable {
|
||||
return inName() || inPackage() || inProcesses()
|
||||
}
|
||||
|
||||
showHidden() || (filterSystem() && filterQuery())
|
||||
showHidden() || (filterSystem() && filterOS() && filterQuery())
|
||||
}
|
||||
state = State.LOADED
|
||||
}
|
||||
@@ -121,4 +122,3 @@ class HideViewModel : BaseViewModel(), Queryable {
|
||||
) }
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -57,7 +57,9 @@ sealed class IconLink : RvItem() {
|
||||
|
||||
object App : PayPal(), AppDev
|
||||
|
||||
object Main : PayPal(), MainDev
|
||||
object Main : PayPal() {
|
||||
override val name: String get() = "magiskdonate"
|
||||
}
|
||||
}
|
||||
|
||||
object Patreon : IconLink() {
|
||||
|
@@ -1,6 +1,5 @@
|
||||
package com.topjohnwu.magisk.ui.home
|
||||
|
||||
import android.os.Build
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
@@ -68,8 +67,7 @@ class HomeViewModel(
|
||||
set(value) = set(value, field, { field = it }, BR.stateManagerProgress)
|
||||
|
||||
@get:Bindable
|
||||
val showUninstall get() =
|
||||
Info.env.magiskVersionCode > 0 && stateMagisk != MagiskState.LOADING && isConnected.get()
|
||||
val showUninstall get() = Info.env.isActive && state != State.LOADING
|
||||
|
||||
@get:Bindable
|
||||
val showSafetyNet get() = Info.hasGMS && isConnected.get()
|
||||
@@ -82,8 +80,6 @@ class HomeViewModel(
|
||||
|
||||
override fun refresh() = viewModelScope.launch {
|
||||
state = State.LOADING
|
||||
notifyPropertyChanged(BR.showUninstall)
|
||||
notifyPropertyChanged(BR.showSafetyNet)
|
||||
svc.fetchUpdate()?.apply {
|
||||
state = State.LOADED
|
||||
stateMagisk = when {
|
||||
@@ -93,7 +89,6 @@ class HomeViewModel(
|
||||
}
|
||||
|
||||
stateManager = when {
|
||||
!app.isUpdateChannelCorrect && isConnected.get() -> MagiskState.NOT_INSTALLED
|
||||
app.isObsolete -> MagiskState.OBSOLETE
|
||||
else -> MagiskState.UP_TO_DATE
|
||||
}
|
||||
@@ -107,6 +102,8 @@ class HomeViewModel(
|
||||
ensureEnv()
|
||||
}
|
||||
} ?: apply { state = State.LOADING_FAILED }
|
||||
notifyPropertyChanged(BR.showUninstall)
|
||||
notifyPropertyChanged(BR.showSafetyNet)
|
||||
}
|
||||
|
||||
val showTest = false
|
||||
@@ -127,14 +124,18 @@ class HomeViewModel(
|
||||
|
||||
fun onDeletePressed() = UninstallDialog().publish()
|
||||
|
||||
fun onManagerPressed() =
|
||||
if (isConnected.get()) ManagerInstallDialog().publish()
|
||||
else SnackbarEvent(R.string.no_connection).publish()
|
||||
fun onManagerPressed() = when (state) {
|
||||
State.LOADED -> ManagerInstallDialog().publish()
|
||||
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||
else -> SnackbarEvent(R.string.no_connection).publish()
|
||||
}
|
||||
|
||||
fun onMagiskPressed() = if (isConnected.get()) withExternalRW {
|
||||
fun onMagiskPressed() = when (state) {
|
||||
State.LOADED -> withExternalRW {
|
||||
HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish()
|
||||
} else {
|
||||
SnackbarEvent(R.string.no_connection).publish()
|
||||
}
|
||||
State.LOADING -> SnackbarEvent(R.string.loading).publish()
|
||||
else -> SnackbarEvent(R.string.no_connection).publish()
|
||||
}
|
||||
|
||||
fun onSafetyNetPressed() =
|
||||
@@ -150,17 +151,7 @@ class HomeViewModel(
|
||||
MagiskState.NOT_INSTALLED,
|
||||
MagiskState.LOADING
|
||||
)
|
||||
|
||||
// Don't bother checking env when magisk is not installed, loading or already has been shown
|
||||
if (
|
||||
invalidStates.any { it == stateMagisk } ||
|
||||
shownDialog ||
|
||||
// don't care for emulators either
|
||||
Build.DEVICE.orEmpty().contains("generic") ||
|
||||
Build.PRODUCT.orEmpty().contains("generic")
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (invalidStates.any { it == stateMagisk } || shownDialog) return
|
||||
|
||||
val result = Shell.su("env_check").await()
|
||||
if (!result.isSuccess) {
|
||||
@@ -171,8 +162,6 @@ class HomeViewModel(
|
||||
|
||||
private val MagiskJson.isObsolete
|
||||
get() = Info.env.isActive && Info.env.magiskVersionCode < versionCode
|
||||
private val ManagerJson.isUpdateChannelCorrect
|
||||
get() = versionCode > 0
|
||||
private val ManagerJson.isObsolete
|
||||
get() = BuildConfig.VERSION_CODE < versionCode
|
||||
|
||||
|
@@ -0,0 +1,88 @@
|
||||
package com.topjohnwu.magisk.ui.inflater
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.InflateException
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.collection.SimpleArrayMap
|
||||
import java.lang.reflect.Constructor
|
||||
|
||||
open class LayoutInflaterFactory(private val delegate: AppCompatDelegate) : LayoutInflater.Factory2 {
|
||||
|
||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||
return onCreateView(null, name, context, attrs)
|
||||
}
|
||||
|
||||
override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
|
||||
val view = delegate.createView(parent, name, context, attrs)
|
||||
?: LayoutInflaterFactoryDefaultImpl.createViewFromTag(context, name, attrs)
|
||||
onViewCreated(view, parent, name, context, attrs)
|
||||
return view
|
||||
}
|
||||
|
||||
open fun onViewCreated(view: View?, parent: View?, name: String, context: Context, attrs: AttributeSet) {
|
||||
if (view == null) return
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
WindowInsetsHelper.attach(view, attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object LayoutInflaterFactoryDefaultImpl {
|
||||
|
||||
private val constructorSignature = arrayOf(
|
||||
Context::class.java, AttributeSet::class.java)
|
||||
|
||||
private val classPrefixList = arrayOf(
|
||||
"android.widget.",
|
||||
"android.view.",
|
||||
"android.webkit."
|
||||
)
|
||||
|
||||
private val constructorMap = SimpleArrayMap<String, Constructor<out View?>>()
|
||||
|
||||
fun createViewFromTag(context: Context, name: String, attrs: AttributeSet): View? {
|
||||
var name = name
|
||||
if (name == "view") {
|
||||
name = attrs.getAttributeValue(null, "class")
|
||||
}
|
||||
return try {
|
||||
if (-1 == name.indexOf('.')) {
|
||||
for (prefix in classPrefixList) {
|
||||
val view: View? = createViewByPrefix(context, name, attrs, prefix)
|
||||
if (view != null) {
|
||||
return view
|
||||
}
|
||||
}
|
||||
null
|
||||
} else {
|
||||
createViewByPrefix(context, name, attrs, null)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(ClassNotFoundException::class, InflateException::class)
|
||||
private fun createViewByPrefix(context: Context, name: String, attrs: AttributeSet, prefix: String?): View? {
|
||||
var constructor = constructorMap[name]
|
||||
return try {
|
||||
if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it
|
||||
val clazz = Class.forName(
|
||||
if (prefix != null) prefix + name else name,
|
||||
false,
|
||||
context.classLoader).asSubclass(View::class.java)
|
||||
constructor = clazz.getConstructor(*constructorSignature)
|
||||
constructorMap.put(name, constructor)
|
||||
}
|
||||
constructor!!.isAccessible = true
|
||||
constructor.newInstance(context, attrs)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,284 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package com.topjohnwu.magisk.ui.inflater
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.annotation.TargetApi
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity.*
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.OnApplyWindowInsetsListener
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.topjohnwu.magisk.R
|
||||
|
||||
private typealias ApplyInsetsCallback<T> = (insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) -> T
|
||||
|
||||
private class ApplyInsets(private val out: Rect) : ApplyInsetsCallback<Unit> {
|
||||
|
||||
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean) {
|
||||
out.left += if (left) insets.left else 0
|
||||
out.top += if (top) insets.top else 0
|
||||
out.right += if (right) insets.right else 0
|
||||
out.bottom += if (bottom) insets.bottom else 0
|
||||
}
|
||||
}
|
||||
|
||||
private class ConsumeInsets : ApplyInsetsCallback<Insets> {
|
||||
|
||||
override fun invoke(insets: Insets, left: Boolean, top: Boolean, right: Boolean, bottom: Boolean): Insets {
|
||||
val insetsLeft = if (left) 0 else insets.left
|
||||
val insetsTop = if (top) 0 else insets.top
|
||||
val insetsRight = if (right) 0 else insets.right
|
||||
val insetsBottom = if (bottom) 0 else insets.bottom
|
||||
return Insets.of(insetsLeft, insetsTop, insetsRight, insetsBottom)
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
open class WindowInsetsHelper private constructor(
|
||||
private val view: View,
|
||||
private val fitSystemWindows: Int,
|
||||
private val layout_fitsSystemWindowsInsets: Int,
|
||||
private val consumeSystemWindows: Int) : OnApplyWindowInsetsListener {
|
||||
|
||||
internal var initialPaddingLeft: Int = view.paddingLeft
|
||||
internal var initialPaddingTop: Int = view.paddingTop
|
||||
internal var initialPaddingRight: Int = view.paddingRight
|
||||
internal var initialPaddingBottom: Int = view.paddingBottom
|
||||
|
||||
private var initialMargin = false
|
||||
internal var initialMarginLeft: Int = 0
|
||||
internal var initialMarginTop: Int = 0
|
||||
internal var initialMarginRight: Int = 0
|
||||
internal var initialMarginBottom: Int = 0
|
||||
internal var initialMarginStart: Int = 0
|
||||
internal var initialMarginEnd: Int = 0
|
||||
|
||||
private var lastInsets: WindowInsetsCompat? = null
|
||||
|
||||
open fun setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
initialPaddingLeft = left
|
||||
initialPaddingTop = top
|
||||
initialPaddingRight = right
|
||||
initialPaddingBottom = bottom
|
||||
|
||||
lastInsets?.let { applyWindowInsets(it) }
|
||||
}
|
||||
|
||||
open fun setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
if (isRTL) {
|
||||
setInitialPadding(start, top, end, bottom)
|
||||
} else {
|
||||
setInitialPadding(start, top, end, bottom)
|
||||
}
|
||||
}
|
||||
|
||||
open fun setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
initialPaddingLeft = left
|
||||
initialPaddingTop = top
|
||||
initialPaddingRight = right
|
||||
initialPaddingBottom = bottom
|
||||
|
||||
lastInsets?.let { applyWindowInsets(it) }
|
||||
}
|
||||
|
||||
open fun setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||
initialMarginStart = start
|
||||
initialMarginTop = top
|
||||
initialMarginEnd = end
|
||||
initialMarginBottom = bottom
|
||||
|
||||
lastInsets?.let { applyWindowInsets(it) }
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private fun <T> applyInsets(insets: Insets, fit: Int, callback: ApplyInsetsCallback<T>): T {
|
||||
val relativeMode = (fit and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION
|
||||
|
||||
val isRTL = view.layoutDirection == View.LAYOUT_DIRECTION_RTL
|
||||
|
||||
val left: Boolean
|
||||
val top = fit and TOP == TOP
|
||||
val right: Boolean
|
||||
val bottom = fit and BOTTOM == BOTTOM
|
||||
|
||||
if (relativeMode) {
|
||||
val start = fit and START == START
|
||||
val end = fit and END == END
|
||||
left = (!isRTL && start) || (isRTL && end)
|
||||
right = (!isRTL && end) || (isRTL && start)
|
||||
} else {
|
||||
left = fit and LEFT == LEFT
|
||||
right = fit and RIGHT == RIGHT
|
||||
}
|
||||
|
||||
return callback.invoke(insets, left, top, right, bottom)
|
||||
}
|
||||
|
||||
private fun applyWindowInsets(windowInsets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
if (fitSystemWindows != 0) {
|
||||
val padding = Rect(initialPaddingLeft, initialPaddingTop, initialPaddingRight, initialPaddingBottom)
|
||||
applyInsets(windowInsets.systemWindowInsets, fitSystemWindows, ApplyInsets(padding))
|
||||
view.setPadding(padding.left, padding.top, padding.right, padding.bottom)
|
||||
}
|
||||
|
||||
if (layout_fitsSystemWindowsInsets != 0) {
|
||||
if (!initialMargin) {
|
||||
initialMarginLeft = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.leftMargin ?: 0
|
||||
initialMarginTop = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin ?: 0
|
||||
initialMarginRight = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.rightMargin ?: 0
|
||||
initialMarginBottom = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.bottomMargin ?: 0
|
||||
initialMarginStart = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart ?: 0
|
||||
initialMarginEnd = (view.layoutParams as? ViewGroup.MarginLayoutParams)?.marginEnd ?: 0
|
||||
initialMargin = true
|
||||
}
|
||||
|
||||
val margin = if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION)
|
||||
Rect(initialMarginLeft, initialMarginTop, initialMarginRight, initialMarginBottom)
|
||||
else
|
||||
Rect(initialMarginStart, initialMarginTop, initialMarginEnd, initialMarginBottom)
|
||||
|
||||
applyInsets(windowInsets.systemWindowInsets, layout_fitsSystemWindowsInsets, ApplyInsets(margin))
|
||||
|
||||
val lp = view.layoutParams
|
||||
if (lp is ViewGroup.MarginLayoutParams) {
|
||||
lp.topMargin = margin.top
|
||||
lp.bottomMargin = margin.bottom
|
||||
|
||||
if ((layout_fitsSystemWindowsInsets and RELATIVE_LAYOUT_DIRECTION) == RELATIVE_LAYOUT_DIRECTION) {
|
||||
lp.marginStart = margin.left
|
||||
lp.marginEnd = margin.right
|
||||
} else {
|
||||
lp.leftMargin = margin.left
|
||||
lp.rightMargin = margin.right
|
||||
}
|
||||
|
||||
view.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
val systemWindowInsets = if (consumeSystemWindows != 0) applyInsets(windowInsets.systemWindowInsets, consumeSystemWindows, ConsumeInsets()) else windowInsets.systemWindowInsets
|
||||
|
||||
return WindowInsetsCompat.Builder(windowInsets)
|
||||
.setSystemWindowInsets(systemWindowInsets)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(view: View, insets: WindowInsetsCompat): WindowInsetsCompat {
|
||||
if (lastInsets == insets) {
|
||||
return insets
|
||||
}
|
||||
|
||||
lastInsets = insets
|
||||
|
||||
return applyWindowInsets(insets)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun attach(view: View, attrs: AttributeSet) {
|
||||
val a = view.context.obtainStyledAttributes(attrs, R.styleable.WindowInsetsHelper, 0, 0)
|
||||
val edgeToEdge = a.getBoolean(R.styleable.WindowInsetsHelper_edgeToEdge, false)
|
||||
val fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_fitsSystemWindowsInsets, 0)
|
||||
val layout_fitsSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_layout_fitsSystemWindowsInsets, 0)
|
||||
val consumeSystemWindowsInsets = a.getInt(R.styleable.WindowInsetsHelper_consumeSystemWindowsInsets, 0)
|
||||
a.recycle()
|
||||
|
||||
attach(view, edgeToEdge, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun attach(view: View, edgeToEdge: Boolean, fitsSystemWindowsInsets: Int, layout_fitsSystemWindowsInsets: Int, consumeSystemWindowsInsets: Int) {
|
||||
if (edgeToEdge) {
|
||||
view.systemUiVisibility = (view.systemUiVisibility
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
}
|
||||
|
||||
if (fitsSystemWindowsInsets == 0 && layout_fitsSystemWindowsInsets == 0 && consumeSystemWindowsInsets == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
val listener = WindowInsetsHelper(view, fitsSystemWindowsInsets, layout_fitsSystemWindowsInsets, consumeSystemWindowsInsets)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, listener)
|
||||
view.setTag(R.id.tag_rikka_material_WindowInsetsHelper, listener)
|
||||
|
||||
if (!view.isAttachedToWindow) {
|
||||
view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
|
||||
override fun onViewAttachedToWindow(v: View) {
|
||||
v.removeOnAttachStateChangeListener(this)
|
||||
v.requestApplyInsets()
|
||||
}
|
||||
|
||||
override fun onViewDetachedFromWindow(v: View) = Unit
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val View.windowInsetsHelper: WindowInsetsHelper?
|
||||
get() {
|
||||
val value = getTag(R.id.tag_rikka_material_WindowInsetsHelper)
|
||||
return if (value is WindowInsetsHelper) value else null
|
||||
}
|
||||
|
||||
val View.initialPaddingLeft: Int
|
||||
get() = windowInsetsHelper?.initialPaddingLeft ?: 0
|
||||
|
||||
val View.initialPaddingTop: Int
|
||||
get() = windowInsetsHelper?.initialPaddingTop ?: 0
|
||||
|
||||
val View.initialPaddingRight: Int
|
||||
get() = windowInsetsHelper?.initialPaddingRight ?: 0
|
||||
|
||||
val View.initialPaddingBottom: Int
|
||||
get() = windowInsetsHelper?.initialPaddingBottom ?: 0
|
||||
|
||||
val View.initialPaddingStart: Int
|
||||
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingRight else initialPaddingLeft
|
||||
|
||||
val View.initialPaddingEnd: Int
|
||||
get() = if (layoutDirection == View.LAYOUT_DIRECTION_RTL) initialPaddingLeft else initialPaddingRight
|
||||
|
||||
fun View.setInitialPadding(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
windowInsetsHelper?.setInitialPadding(left, top, right, bottom)
|
||||
}
|
||||
|
||||
fun View.setInitialPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||
windowInsetsHelper?.setInitialPaddingRelative(start, top, end, bottom)
|
||||
}
|
||||
|
||||
val View.initialMarginLeft: Int
|
||||
get() = windowInsetsHelper?.initialMarginLeft ?: 0
|
||||
|
||||
val View.initialMarginTop: Int
|
||||
get() = windowInsetsHelper?.initialMarginTop ?: 0
|
||||
|
||||
val View.initialMarginRight: Int
|
||||
get() = windowInsetsHelper?.initialMarginRight ?: 0
|
||||
|
||||
val View.initialMarginBottom: Int
|
||||
get() = windowInsetsHelper?.initialMarginBottom ?: 0
|
||||
|
||||
val View.initialMarginStart: Int
|
||||
get() = windowInsetsHelper?.initialMarginStart ?: 0
|
||||
|
||||
val View.initialMarginEnd: Int
|
||||
get() = windowInsetsHelper?.initialMarginEnd ?: 0
|
||||
|
||||
fun View.setInitialMargin(left: Int, top: Int, right: Int, bottom: Int) {
|
||||
windowInsetsHelper?.setInitialMargin(left, top, right, bottom)
|
||||
}
|
||||
|
||||
fun View.setInitialMarginRelative(start: Int, top: Int, end: Int, bottom: Int) {
|
||||
windowInsetsHelper?.setInitialMarginRelative(start, top, end, bottom)
|
||||
}
|
@@ -18,6 +18,8 @@ import com.topjohnwu.magisk.utils.set
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.get
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class InstallViewModel(
|
||||
@@ -64,7 +66,11 @@ class InstallViewModel(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
notes = svc.fetchString(Info.remote.magisk.note)
|
||||
} catch (e: IOException) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +81,8 @@ class InstallViewModel(
|
||||
this.progress = progress.times(100).roundToInt()
|
||||
if (this.progress >= 100) {
|
||||
state = State.LOADED
|
||||
} else if (this.progress < -150) {
|
||||
state = State.LOADING_FAILED
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,9 @@ import androidx.core.view.isVisible
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.databinding.FragmentLogMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
@@ -42,6 +45,21 @@ class LogFragment : BaseUIFragment<LogViewModel, FragmentLogMd2Binding>() {
|
||||
binding.logFilterToggle.setOnClickListener {
|
||||
isMagiskLogVisible = true
|
||||
}
|
||||
|
||||
val resource = requireContext().resources
|
||||
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||
binding.logFilterSuperuser.logSuperuser.addVerticalPadding(
|
||||
0,
|
||||
l1
|
||||
)
|
||||
binding.logFilterSuperuser.logSuperuser.addSimpleItemDecoration(
|
||||
left = l1,
|
||||
top = l_50,
|
||||
right = l1,
|
||||
bottom = l_50,
|
||||
)
|
||||
binding.logFilterSuperuser.logSuperuser.fixEdgeEffect()
|
||||
}
|
||||
|
||||
|
||||
|
@@ -3,20 +3,24 @@ package com.topjohnwu.magisk.ui.log
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.BuildConfig
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseViewModel
|
||||
import com.topjohnwu.magisk.arch.diffListOf
|
||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils.outputStream
|
||||
import com.topjohnwu.magisk.data.repository.LogRepository
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.ktx.now
|
||||
import com.topjohnwu.magisk.ktx.timeFormatStandard
|
||||
import com.topjohnwu.magisk.ktx.toTime
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
import com.topjohnwu.magisk.view.TextItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
class LogViewModel(
|
||||
private val repo: LogRepository
|
||||
@@ -54,14 +58,23 @@ class LogViewModel(
|
||||
|
||||
fun saveMagiskLog() = withExternalRW {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val now = Calendar.getInstance()
|
||||
val filename = "magisk_log_%04d%02d%02d_%02d%02d%02d.log".format(
|
||||
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)
|
||||
)
|
||||
val logFile = MediaStoreUtils.getFile(filename)
|
||||
logFile.uri.outputStream().writer().use { it.write(consoleText) }
|
||||
val filename = "magisk_log_%s.log".format(now.toTime(timeFormatStandard))
|
||||
val logFile = MediaStoreUtils.getFile(filename, true)
|
||||
logFile.uri.outputStream().bufferedWriter().use { file ->
|
||||
file.write("---System Properties---\n\n")
|
||||
|
||||
ProcessBuilder("getprop").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
|
||||
file.write("\n---Magisk Logs---\n")
|
||||
file.write("${Info.env.magiskVersionString} (${Info.env.magiskVersionCode})\n\n")
|
||||
file.write(consoleText)
|
||||
|
||||
file.write("\n---Manager Logs---\n")
|
||||
file.write("${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})\n\n")
|
||||
ProcessBuilder("logcat", "-d").start()
|
||||
.inputStream.reader().use { it.copyTo(file) }
|
||||
}
|
||||
SnackbarEvent(logFile.toString()).publish()
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ import com.topjohnwu.magisk.arch.ReselectionTarget
|
||||
import com.topjohnwu.magisk.arch.ViewEvent
|
||||
import com.topjohnwu.magisk.core.download.BaseDownloader
|
||||
import com.topjohnwu.magisk.databinding.FragmentModuleMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.hideKeyboard
|
||||
import com.topjohnwu.magisk.ktx.*
|
||||
import com.topjohnwu.magisk.ui.MainActivity
|
||||
import com.topjohnwu.magisk.utils.EndlessRecyclerScrollListener
|
||||
import com.topjohnwu.magisk.utils.MotionRevealHelper
|
||||
@@ -62,6 +62,40 @@ class ModuleFragment : BaseUIFragment<ModuleViewModel, FragmentModuleMd2Binding>
|
||||
if (newState != RecyclerView.SCROLL_STATE_IDLE) hideKeyboard()
|
||||
}
|
||||
})
|
||||
|
||||
val resource = requireContext().resources
|
||||
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||
binding.moduleList.apply {
|
||||
addVerticalPadding(
|
||||
l_50,
|
||||
l1 + l_50 + resource.getDimensionPixelSize(R.dimen.internal_action_bar_size)
|
||||
)
|
||||
addSimpleItemDecoration(
|
||||
left = l1,
|
||||
top = l_50,
|
||||
right = l1,
|
||||
bottom = l_50,
|
||||
)
|
||||
fixEdgeEffect()
|
||||
post {
|
||||
addInvalidateItemDecorationsObserver()
|
||||
}
|
||||
}
|
||||
|
||||
binding.moduleFilterInclude.moduleFilterList.apply {
|
||||
addVerticalPadding(
|
||||
l_50,
|
||||
l_50
|
||||
)
|
||||
addSimpleItemDecoration(
|
||||
left = l1,
|
||||
top = l_50,
|
||||
right = l1,
|
||||
bottom = l_50,
|
||||
)
|
||||
fixEdgeEffect()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
|
@@ -3,8 +3,8 @@ package com.topjohnwu.magisk.ui.module
|
||||
import androidx.databinding.Bindable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.core.model.module.Module
|
||||
import com.topjohnwu.magisk.core.model.module.Repo
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.model.module.OnlineModule
|
||||
import com.topjohnwu.magisk.databinding.ComparableRvItem
|
||||
import com.topjohnwu.magisk.databinding.ObservableItem
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
@@ -39,7 +39,7 @@ class SectionTitle(
|
||||
override fun contentSameAs(other: SectionTitle): Boolean = this === other
|
||||
}
|
||||
|
||||
sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
|
||||
sealed class RepoItem(val item: OnlineModule) : ObservableItem<RepoItem>() {
|
||||
override val layoutRes: Int = R.layout.item_repo_md2
|
||||
|
||||
@get:Bindable
|
||||
@@ -51,21 +51,21 @@ sealed class RepoItem(val item: Repo) : ObservableItem<RepoItem>() {
|
||||
override fun contentSameAs(other: RepoItem): Boolean = item == other.item
|
||||
override fun itemSameAs(other: RepoItem): Boolean = item.id == other.item.id
|
||||
|
||||
class Update(item: Repo) : RepoItem(item) {
|
||||
class Update(item: OnlineModule) : RepoItem(item) {
|
||||
override val isUpdate get() = true
|
||||
}
|
||||
|
||||
class Remote(item: Repo) : RepoItem(item) {
|
||||
class Remote(item: OnlineModule) : RepoItem(item) {
|
||||
override val isUpdate get() = false
|
||||
}
|
||||
}
|
||||
|
||||
class ModuleItem(val item: Module) : ObservableItem<ModuleItem>() {
|
||||
class ModuleItem(val item: LocalModule) : ObservableItem<ModuleItem>() {
|
||||
|
||||
override val layoutRes = R.layout.item_module_md2
|
||||
|
||||
@get:Bindable
|
||||
var repo: Repo? = null
|
||||
var repo: OnlineModule? = null
|
||||
set(value) = set(value, field, { field = it }, BR.repo)
|
||||
|
||||
@get:Bindable
|
||||
|
@@ -9,13 +9,12 @@ import com.topjohnwu.magisk.arch.*
|
||||
import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.model.module.Module
|
||||
import com.topjohnwu.magisk.core.model.module.LocalModule
|
||||
import com.topjohnwu.magisk.core.tasks.RepoUpdater
|
||||
import com.topjohnwu.magisk.data.database.RepoByNameDao
|
||||
import com.topjohnwu.magisk.data.database.RepoByUpdatedDao
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.databinding.RvItem
|
||||
import com.topjohnwu.magisk.events.OpenReadmeEvent
|
||||
import com.topjohnwu.magisk.events.SelectModuleEvent
|
||||
import com.topjohnwu.magisk.events.OpenChangelogEvent
|
||||
import com.topjohnwu.magisk.events.SnackbarEvent
|
||||
import com.topjohnwu.magisk.events.dialog.ModuleInstallDialog
|
||||
import com.topjohnwu.magisk.ktx.addOnListChangedCallback
|
||||
@@ -44,8 +43,7 @@ import kotlin.math.roundToInt
|
||||
* */
|
||||
|
||||
class ModuleViewModel(
|
||||
private val repoName: RepoByNameDao,
|
||||
private val repoUpdated: RepoByUpdatedDao,
|
||||
private val repoDB: RepoDao,
|
||||
private val repoUpdater: RepoUpdater
|
||||
) : BaseViewModel(), Queryable {
|
||||
|
||||
@@ -117,12 +115,6 @@ class ModuleViewModel(
|
||||
// ---
|
||||
|
||||
private var refetch = false
|
||||
private val dao
|
||||
get() = when (Config.repoOrder) {
|
||||
Config.Value.ORDER_DATE -> repoUpdated
|
||||
Config.Value.ORDER_NAME -> repoName
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
@@ -186,7 +178,7 @@ class ModuleViewModel(
|
||||
}
|
||||
|
||||
private suspend fun loadInstalled() {
|
||||
val installed = Module.installed().map { ModuleItem(it) }
|
||||
val installed = LocalModule.installed().map { ModuleItem(it) }
|
||||
val diff = withContext(Dispatchers.Default) {
|
||||
itemsInstalled.calculateDiff(installed)
|
||||
}
|
||||
@@ -197,11 +189,11 @@ class ModuleViewModel(
|
||||
val (updates, diff) = withContext(Dispatchers.IO) {
|
||||
itemsInstalled.forEach {
|
||||
launch {
|
||||
it.repo = dao.getRepoById(it.item.id)
|
||||
it.repo = repoDB.getModule(it.item.id)
|
||||
}
|
||||
}
|
||||
val updates = itemsInstalled
|
||||
.mapNotNull { dao.getUpdatableRepoById(it.item.id, it.item.versionCode) }
|
||||
.mapNotNull { repoDB.getUpdatableModule(it.item.id, it.item.versionCode) }
|
||||
.map { RepoItem.Update(it) }
|
||||
val diff = itemsUpdatable.calculateDiff(updates)
|
||||
return@withContext updates to diff
|
||||
@@ -219,7 +211,7 @@ class ModuleViewModel(
|
||||
|
||||
remoteJob = viewModelScope.launch {
|
||||
suspend fun loadRemoteDB(offset: Int) = withContext(Dispatchers.IO) {
|
||||
dao.getRepos(offset).map { RepoItem.Remote(it) }
|
||||
repoDB.getModules(offset).map { RepoItem.Remote(it) }
|
||||
}
|
||||
|
||||
isRemoteLoading = true
|
||||
@@ -253,7 +245,7 @@ class ModuleViewModel(
|
||||
listOf()
|
||||
} else {
|
||||
withContext(Dispatchers.IO) {
|
||||
dao.searchRepos(query, offset).map { RepoItem.Remote(it) }
|
||||
repoDB.searchModules(query, offset).map { RepoItem.Remote(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,14 +307,14 @@ class ModuleViewModel(
|
||||
}
|
||||
|
||||
fun infoPressed(item: RepoItem) =
|
||||
if (isConnected.get()) OpenChangelogEvent(item.item).publish()
|
||||
if (isConnected.get()) OpenReadmeEvent(item.item).publish()
|
||||
else SnackbarEvent(R.string.no_connection).publish()
|
||||
|
||||
|
||||
fun infoPressed(item: ModuleItem) {
|
||||
item.repo?.also {
|
||||
if (isConnected.get())
|
||||
OpenChangelogEvent(it).publish()
|
||||
OpenReadmeEvent(it).publish()
|
||||
else
|
||||
SnackbarEvent(R.string.no_connection).publish()
|
||||
} ?: return
|
||||
|
@@ -158,7 +158,7 @@ sealed class BaseSettingsItem : ObservableItem<BaseSettingsItem>() {
|
||||
runCatching { getStringArray(id) }.getOrDefault(emptyArray())
|
||||
|
||||
override fun onPressed(view: View, callback: Callback) {
|
||||
if (entries.isEmpty() || entryValues.isEmpty()) return
|
||||
if (entries.isEmpty()) return
|
||||
super.onPressed(view, callback)
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,9 @@ import android.view.View
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.databinding.FragmentSettingsMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||
import com.topjohnwu.magisk.ktx.setOnViewReadyListener
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
@@ -24,6 +27,21 @@ class SettingsFragment : BaseUIFragment<SettingsViewModel, FragmentSettingsMd2Bi
|
||||
binding.settingsList.setOnViewReadyListener {
|
||||
binding.settingsList.scrollToPosition(0)
|
||||
}
|
||||
|
||||
val resource = requireContext().resources
|
||||
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||
binding.settingsList.addVerticalPadding(
|
||||
0,
|
||||
l1
|
||||
)
|
||||
binding.settingsList.addSimpleItemDecoration(
|
||||
left = l1,
|
||||
top = l_50,
|
||||
right = l1,
|
||||
bottom = l_50,
|
||||
)
|
||||
binding.settingsList.fixEdgeEffect()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@@ -11,7 +11,7 @@ import com.topjohnwu.magisk.core.Config
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.UpdateCheckService
|
||||
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.core.utils.BiometricHelper
|
||||
import com.topjohnwu.magisk.core.utils.MediaStoreUtils
|
||||
import com.topjohnwu.magisk.core.utils.availableLocales
|
||||
@@ -20,6 +20,7 @@ import com.topjohnwu.magisk.databinding.DialogSettingsAppNameBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsDownloadPathBinding
|
||||
import com.topjohnwu.magisk.databinding.DialogSettingsUpdateChannelBinding
|
||||
import com.topjohnwu.magisk.ktx.get
|
||||
import com.topjohnwu.magisk.utils.TransitiveText
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.asTransitive
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
@@ -90,7 +91,7 @@ object Hide : BaseSettingsItem.Input() {
|
||||
set(value) = set(value, field, { field = it }, BR.result, BR.error)
|
||||
|
||||
val maxLength
|
||||
get() = PatchAPK.MAX_LABEL_LENGTH
|
||||
get() = HideAPK.MAX_LABEL_LENGTH
|
||||
|
||||
@get:Bindable
|
||||
val isError
|
||||
@@ -99,11 +100,17 @@ object Hide : BaseSettingsItem.Input() {
|
||||
override fun getView(context: Context) = DialogSettingsAppNameBinding
|
||||
.inflate(LayoutInflater.from(context)).also { it.data = this }.root
|
||||
|
||||
override fun refresh() {
|
||||
isEnabled = Info.remote.stub.versionCode > 0
|
||||
}
|
||||
}
|
||||
|
||||
object Restore : BaseSettingsItem.Blank() {
|
||||
override val title = R.string.settings_restore_manager_title.asTransitive()
|
||||
override val description = R.string.settings_restore_manager_summary.asTransitive()
|
||||
override fun refresh() {
|
||||
isEnabled = Info.remote.app.versionCode > 0
|
||||
}
|
||||
}
|
||||
|
||||
object AddShortcut : BaseSettingsItem.Blank() {
|
||||
@@ -137,11 +144,12 @@ object UpdateChannel : BaseSettingsItem.Selector() {
|
||||
set(value) = setV(value, field, { field = it }) { Config.updateChannel = it }
|
||||
|
||||
override val title = R.string.settings_update_channel_title.asTransitive()
|
||||
override val entries
|
||||
get() = resources.getStringArray(R.array.update_channel).let {
|
||||
override val entries: Array<String> = resources.getStringArray(R.array.update_channel).let {
|
||||
if (BuildConfig.DEBUG) it.toMutableList().apply { add("Canary") }.toTypedArray() else it
|
||||
}
|
||||
override val entryValRes = R.array.value_array
|
||||
override val description
|
||||
get() = entries.getOrNull(value)?.asTransitive()
|
||||
?: TransitiveText.String(if (value == -1) entries[0] else "Canary")
|
||||
}
|
||||
|
||||
object UpdateChannelUrl : BaseSettingsItem.Input() {
|
||||
@@ -189,6 +197,13 @@ object SystemlessHosts : BaseSettingsItem.Blank() {
|
||||
override val description = R.string.settings_hosts_summary.asTransitive()
|
||||
}
|
||||
|
||||
object Tapjack : BaseSettingsItem.Toggle() {
|
||||
override val title = R.string.settings_su_tapjack_title.asTransitive()
|
||||
override var description = R.string.settings_su_tapjack_summary.asTransitive()
|
||||
override var value = Config.suTapjack
|
||||
set(value) = setV(value, field, { field = it }) { Config.suTapjack = it }
|
||||
}
|
||||
|
||||
object Biometrics : BaseSettingsItem.Toggle() {
|
||||
override val title = R.string.settings_su_biometric_title.asTransitive()
|
||||
override var value = Config.suBiometric
|
||||
@@ -243,22 +258,20 @@ object Superuser : BaseSettingsItem.Section() {
|
||||
object AccessMode : BaseSettingsItem.Selector() {
|
||||
override val title = R.string.superuser_access.asTransitive()
|
||||
override val entryRes = R.array.su_access
|
||||
override val entryValRes = R.array.value_array
|
||||
|
||||
override var value = Config.rootMode
|
||||
set(value) = setV(value, field, { field = it }) {
|
||||
Config.rootMode = entryValues[it].toInt()
|
||||
Config.rootMode = it
|
||||
}
|
||||
}
|
||||
|
||||
object MultiuserMode : BaseSettingsItem.Selector() {
|
||||
override val title = R.string.multiuser_mode.asTransitive()
|
||||
override val entryRes = R.array.multiuser_mode
|
||||
override val entryValRes = R.array.value_array
|
||||
|
||||
override var value = Config.suMultiuserMode
|
||||
set(value) = setV(value, field, { field = it }) {
|
||||
Config.suMultiuserMode = entryValues[it].toInt()
|
||||
Config.suMultiuserMode = it
|
||||
}
|
||||
|
||||
override val description
|
||||
@@ -272,11 +285,10 @@ object MultiuserMode : BaseSettingsItem.Selector() {
|
||||
object MountNamespaceMode : BaseSettingsItem.Selector() {
|
||||
override val title = R.string.mount_namespace_mode.asTransitive()
|
||||
override val entryRes = R.array.namespace
|
||||
override val entryValRes = R.array.value_array
|
||||
|
||||
override var value = Config.suMntNamespaceMode
|
||||
set(value) = setV(value, field, { field = it }) {
|
||||
Config.suMntNamespaceMode = entryValues[it].toInt()
|
||||
Config.suMntNamespaceMode = it
|
||||
}
|
||||
|
||||
override val description
|
||||
@@ -286,11 +298,10 @@ object MountNamespaceMode : BaseSettingsItem.Selector() {
|
||||
object AutomaticResponse : BaseSettingsItem.Selector() {
|
||||
override val title = R.string.auto_response.asTransitive()
|
||||
override val entryRes = R.array.auto_response
|
||||
override val entryValRes = R.array.value_array
|
||||
|
||||
override var value = Config.suAutoReponse
|
||||
override var value = Config.suAutoResponse
|
||||
set(value) = setV(value, field, { field = it }) {
|
||||
Config.suAutoReponse = entryValues[it].toInt()
|
||||
Config.suAutoResponse = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,10 +322,9 @@ object RequestTimeout : BaseSettingsItem.Selector() {
|
||||
object SUNotification : BaseSettingsItem.Selector() {
|
||||
override val title = R.string.superuser_notification.asTransitive()
|
||||
override val entryRes = R.array.su_notification
|
||||
override val entryValRes = R.array.value_array
|
||||
|
||||
override var value = Config.suNotification
|
||||
set(value) = setV(value, field, { field = it }) {
|
||||
Config.suNotification = entryValues[it].toInt()
|
||||
Config.suNotification = it
|
||||
}
|
||||
}
|
||||
|
@@ -15,11 +15,8 @@ import com.topjohnwu.magisk.arch.diffListOf
|
||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||
import com.topjohnwu.magisk.core.Const
|
||||
import com.topjohnwu.magisk.core.Info
|
||||
import com.topjohnwu.magisk.core.download.Action
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.isRunningAsStub
|
||||
import com.topjohnwu.magisk.core.tasks.PatchAPK
|
||||
import com.topjohnwu.magisk.core.tasks.HideAPK
|
||||
import com.topjohnwu.magisk.data.database.RepoDao
|
||||
import com.topjohnwu.magisk.events.AddHomeIconEvent
|
||||
import com.topjohnwu.magisk.events.RecreateEvent
|
||||
@@ -82,7 +79,7 @@ class SettingsViewModel(
|
||||
if (Utils.showSuperUser()) {
|
||||
list.addAll(listOf(
|
||||
Superuser,
|
||||
Biometrics, AccessMode, MultiuserMode, MountNamespaceMode,
|
||||
Tapjack, Biometrics, AccessMode, MultiuserMode, MountNamespaceMode,
|
||||
AutomaticResponse, RequestTimeout, SUNotification
|
||||
))
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
@@ -104,7 +101,7 @@ class SettingsViewModel(
|
||||
is Theme -> SettingsFragmentDirections.actionSettingsFragmentToThemeFragment().publish()
|
||||
is ClearRepoCache -> clearRepoCache()
|
||||
is SystemlessHosts -> createHosts()
|
||||
is Restore -> restoreManager()
|
||||
is Restore -> HideAPK.restore(view.context)
|
||||
is AddShortcut -> AddHomeIconEvent().publish()
|
||||
else -> callback()
|
||||
}
|
||||
@@ -112,7 +109,7 @@ class SettingsViewModel(
|
||||
override fun onItemChanged(view: View, item: BaseSettingsItem) = when (item) {
|
||||
is Language -> RecreateEvent().publish()
|
||||
is UpdateChannel -> openUrlIfNecessary(view)
|
||||
is Hide -> PatchAPK.hideManager(view.context, item.value)
|
||||
is Hide -> HideAPK.hide(view.context, item.value)
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
@@ -142,9 +139,4 @@ class SettingsViewModel(
|
||||
Utils.toast(R.string.settings_hosts_toast, Toast.LENGTH_SHORT)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreManager() {
|
||||
DownloadService.start(get(), Subject.Manager(Action.APK.Restore))
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,8 +1,13 @@
|
||||
package com.topjohnwu.magisk.ui.superuser
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.BaseUIFragment
|
||||
import com.topjohnwu.magisk.databinding.FragmentSuperuserMd2Binding
|
||||
import com.topjohnwu.magisk.ktx.addSimpleItemDecoration
|
||||
import com.topjohnwu.magisk.ktx.addVerticalPadding
|
||||
import com.topjohnwu.magisk.ktx.fixEdgeEffect
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
|
||||
class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd2Binding>() {
|
||||
@@ -15,6 +20,25 @@ class SuperuserFragment : BaseUIFragment<SuperuserViewModel, FragmentSuperuserMd
|
||||
activity.title = resources.getString(R.string.superuser)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val resource = requireContext().resources
|
||||
val l_50 = resource.getDimensionPixelSize(R.dimen.l_50)
|
||||
val l1 = resource.getDimensionPixelSize(R.dimen.l1)
|
||||
binding.superuserList.addVerticalPadding(
|
||||
l_50,
|
||||
l1
|
||||
)
|
||||
binding.superuserList.addSimpleItemDecoration(
|
||||
left = l1,
|
||||
top = l_50,
|
||||
right = l1,
|
||||
bottom = l_50,
|
||||
)
|
||||
binding.superuserList.fixEdgeEffect()
|
||||
}
|
||||
|
||||
override fun onPreBind(binding: FragmentSuperuserMd2Binding) {}
|
||||
|
||||
}
|
||||
|
@@ -28,8 +28,8 @@ open class SuRequestActivity : BaseUIActivity<SuRequestViewModel, ActivityReques
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
supportRequestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
lockOrientation()
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
window.addFlags(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
fun showRequest() {
|
||||
|
@@ -1,11 +1,20 @@
|
||||
package com.topjohnwu.magisk.ui.surequest
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.os.CountDownTimer
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
import android.view.accessibility.AccessibilityNodeProvider
|
||||
import android.widget.Toast
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.topjohnwu.magisk.BR
|
||||
@@ -22,6 +31,7 @@ import com.topjohnwu.magisk.events.DieEvent
|
||||
import com.topjohnwu.magisk.events.ShowUIEvent
|
||||
import com.topjohnwu.magisk.events.dialog.BiometricEvent
|
||||
import com.topjohnwu.magisk.ui.superuser.SpinnerRvItem
|
||||
import com.topjohnwu.magisk.utils.Utils
|
||||
import com.topjohnwu.magisk.utils.set
|
||||
import kotlinx.coroutines.launch
|
||||
import me.tatarka.bindingcollectionadapter2.BindingListViewAdapter
|
||||
@@ -51,6 +61,19 @@ class SuRequestViewModel(
|
||||
var grantEnabled = false
|
||||
set(value) = set(value, field, { field = it }, BR.grantEnabled)
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
val grantTouchListener = View.OnTouchListener { _: View, event: MotionEvent ->
|
||||
// Filter obscured touches by consuming them.
|
||||
if (event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0
|
||||
|| event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0) {
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
Utils.toast(R.string.touch_filtered_warning, Toast.LENGTH_SHORT)
|
||||
}
|
||||
return@OnTouchListener Config.suTapjack
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
private val items = res.getStringArray(R.array.allow_timeout).map { SpinnerRvItem(it) }
|
||||
val adapter = BindingListViewAdapter<SpinnerRvItem>(1).apply {
|
||||
itemBinding = ItemBinding.of { binding, _, item ->
|
||||
@@ -104,7 +127,7 @@ class SuRequestViewModel(
|
||||
timer = SuTimer(millis, 1000).apply { start() }
|
||||
|
||||
// Actually show the UI
|
||||
ShowUIEvent().publish()
|
||||
ShowUIEvent(if (Config.suTapjack) EmptyAccessibilityDelegate else null).publish()
|
||||
}
|
||||
|
||||
private fun respond(action: Int) {
|
||||
@@ -141,4 +164,18 @@ class SuRequestViewModel(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Invisible for accessibility services
|
||||
object EmptyAccessibilityDelegate : View.AccessibilityDelegate() {
|
||||
override fun sendAccessibilityEvent(host: View?, eventType: Int) {}
|
||||
override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?) = true
|
||||
override fun sendAccessibilityEventUnchecked(host: View?, event: AccessibilityEvent?) {}
|
||||
override fun dispatchPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) = true
|
||||
override fun onPopulateAccessibilityEvent(host: View?, event: AccessibilityEvent?) {}
|
||||
override fun onInitializeAccessibilityEvent(host: View?, event: AccessibilityEvent?) {}
|
||||
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo) {}
|
||||
override fun addExtraDataToAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfo, extraDataKey: String, arguments: Bundle?) {}
|
||||
override fun onRequestSendAccessibilityEvent(host: ViewGroup?, child: View?, event: AccessibilityEvent?): Boolean = false
|
||||
override fun getAccessibilityNodeProvider(host: View?): AccessibilityNodeProvider? = null
|
||||
}
|
||||
}
|
||||
|
@@ -55,15 +55,15 @@ class EndlessRecyclerScrollListener(
|
||||
val visibleItemPosition = if (direction == Direction.BOTTOM) {
|
||||
when (layoutManager) {
|
||||
is StaggeredGridLayoutManager ->
|
||||
layoutManager.findLastVisibleItemPositions(null).max() ?: 0
|
||||
layoutManager.findLastVisibleItemPositions(null).maxOrNull() ?: 0
|
||||
is GridLayoutManager -> layoutManager.findLastVisibleItemPosition()
|
||||
is LinearLayoutManager -> layoutManager.findLastVisibleItemPosition()
|
||||
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
|
||||
}
|
||||
} else {
|
||||
when (layoutManager) {
|
||||
is StaggeredGridLayoutManager -> layoutManager.findFirstVisibleItemPositions(null).min()
|
||||
?: 0
|
||||
is StaggeredGridLayoutManager ->
|
||||
layoutManager.findFirstVisibleItemPositions(null).minOrNull() ?: 0
|
||||
is GridLayoutManager -> layoutManager.findFirstVisibleItemPosition()
|
||||
is LinearLayoutManager -> layoutManager.findFirstVisibleItemPosition()
|
||||
else -> throw IllegalArgumentException("Only LinearLayoutManager, GridLayoutManager and StaggeredGridLayoutManager are supported")
|
||||
|
@@ -2,25 +2,24 @@ package com.topjohnwu.magisk.view
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.InsetDrawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatDialog
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.databinding.Bindable
|
||||
import androidx.databinding.PropertyChangeRegistry
|
||||
import androidx.databinding.ViewDataBinding
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.topjohnwu.magisk.BR
|
||||
import com.topjohnwu.magisk.R
|
||||
import com.topjohnwu.magisk.arch.itemBindingOf
|
||||
@@ -47,33 +46,19 @@ class MagiskDialog(
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
super.setContentView(binding.root)
|
||||
|
||||
val surfaceColor = MaterialColors.getColor(context, R.attr.colorSurfaceSurfaceVariant, javaClass.canonicalName)
|
||||
val materialShapeDrawable = MaterialShapeDrawable(context, null, R.attr.alertDialogStyle, R.style.MaterialAlertDialog_MaterialComponents)
|
||||
materialShapeDrawable.initializeElevationOverlay(context)
|
||||
materialShapeDrawable.fillColor = ColorStateList.valueOf(surfaceColor)
|
||||
materialShapeDrawable.elevation = context.resources.getDimension(R.dimen.margin_generic)
|
||||
materialShapeDrawable.setCornerSize(context.resources.getDimension(R.dimen.l_50))
|
||||
|
||||
val inset = context.resources.getDimensionPixelSize(R.dimen.appcompat_dialog_background_inset)
|
||||
window?.apply {
|
||||
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
setBackgroundDrawable(InsetDrawable(materialShapeDrawable, inset, inset, inset, inset))
|
||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
}
|
||||
|
||||
val paddingTop = binding.root.paddingTop
|
||||
val paddingBottom = binding.root.paddingBottom
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { view, insets ->
|
||||
view.updatePadding(
|
||||
top = paddingTop + insets.systemWindowInsetTop,
|
||||
bottom = paddingBottom + insets.systemWindowInsetBottom
|
||||
)
|
||||
insets
|
||||
}
|
||||
}
|
||||
|
||||
override fun setCancelable(flag: Boolean) {
|
||||
val listener = if (!flag) {
|
||||
null
|
||||
} else {
|
||||
setCanceledOnTouchOutside(true)
|
||||
View.OnClickListener { dismiss() }
|
||||
}
|
||||
binding.dialogBaseOutsideContainer.setOnClickListener(listener)
|
||||
}
|
||||
|
||||
inner class Data : ObservableHost {
|
||||
@@ -228,7 +213,7 @@ class MagiskDialog(
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
val items = list.mapIndexed { i, it -> DialogItem(it, i) }
|
||||
val items = list.mapIndexed { i, cs -> DialogItem(cs, i) }
|
||||
val binding = itemBindingOf<DialogItem> { it.bindExtra(BR.listener, actualListener) }
|
||||
.let { ItemBinding.of(it) }
|
||||
|
||||
|
@@ -15,7 +15,6 @@ import com.topjohnwu.magisk.core.Const.ID.PROGRESS_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.core.Const.ID.UPDATE_NOTIFICATION_CHANNEL
|
||||
import com.topjohnwu.magisk.core.SplashActivity
|
||||
import com.topjohnwu.magisk.core.cmp
|
||||
import com.topjohnwu.magisk.core.download.Action
|
||||
import com.topjohnwu.magisk.core.download.DownloadService
|
||||
import com.topjohnwu.magisk.core.download.Subject
|
||||
import com.topjohnwu.magisk.core.intent
|
||||
@@ -46,7 +45,6 @@ object Notifications {
|
||||
setChannelId(UPDATE_NOTIFICATION_CHANNEL)
|
||||
} else {
|
||||
setSmallIcon(R.drawable.ic_magisk_outline)
|
||||
setVibrate(longArrayOf(0, 100, 100, 100))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +68,7 @@ object Notifications {
|
||||
}
|
||||
|
||||
fun managerUpdate(context: Context) {
|
||||
val intent = DownloadService.pendingIntent(context, Subject.Manager(Action.APK.Upgrade))
|
||||
val intent = DownloadService.pendingIntent(context, Subject.Manager())
|
||||
|
||||
val builder = updateBuilder(context)
|
||||
.setContentTitle(context.getString(R.string.manager_update_title))
|
||||
|
@@ -1,5 +1,8 @@
|
||||
package com.topjohnwu.signing;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.bouncycastle.asn1.ASN1EncodableVector;
|
||||
import org.bouncycastle.asn1.ASN1InputStream;
|
||||
@@ -14,6 +17,7 @@ import org.bouncycastle.asn1.DERSequence;
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -84,8 +88,10 @@ public class SignBoot {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean doSignature(String target, InputStream imgIn, OutputStream imgOut,
|
||||
InputStream cert, InputStream key) {
|
||||
public static boolean doSignature(
|
||||
@Nullable X509Certificate cert, @Nullable PrivateKey key,
|
||||
@NonNull InputStream imgIn, @NonNull OutputStream imgOut, @NonNull String target
|
||||
) {
|
||||
try {
|
||||
PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
|
||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
||||
@@ -96,16 +102,16 @@ public class SignBoot {
|
||||
in.unread(hdr);
|
||||
BootSignature bootsig = new BootSignature(target, signableSize);
|
||||
if (cert == null) {
|
||||
cert = SignBoot.class.getResourceAsStream("/keys/verity.x509.pem");
|
||||
cert = CryptoUtils.readCertificate(
|
||||
new ByteArrayInputStream(KeyData.verityCert()));
|
||||
}
|
||||
X509Certificate certificate = CryptoUtils.readCertificate(cert);
|
||||
bootsig.setCertificate(certificate);
|
||||
bootsig.setCertificate(cert);
|
||||
if (key == null) {
|
||||
key = SignBoot.class.getResourceAsStream("/keys/verity.pk8");
|
||||
key = CryptoUtils.readPrivateKey(
|
||||
new ByteArrayInputStream(KeyData.verityKey()));
|
||||
}
|
||||
PrivateKey privateKey = CryptoUtils.readPrivateKey(key);
|
||||
byte[] sig = bootsig.sign(privateKey, in, signableSize);
|
||||
bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(privateKey));
|
||||
byte[] sig = bootsig.sign(key, in, signableSize);
|
||||
bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(key));
|
||||
byte[] encoded_bootsig = bootsig.getEncoded();
|
||||
imgOut.write(encoded_bootsig);
|
||||
imgOut.flush();
|
||||
@@ -116,7 +122,7 @@ public class SignBoot {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean verifySignature(InputStream imgIn, InputStream certIn) {
|
||||
public static boolean verifySignature(InputStream imgIn, X509Certificate cert) {
|
||||
try {
|
||||
// Read the header for size
|
||||
byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
|
||||
@@ -142,8 +148,8 @@ public class SignBoot {
|
||||
}
|
||||
|
||||
BootSignature bootsig = new BootSignature(signature);
|
||||
if (certIn != null) {
|
||||
bootsig.setCertificate(CryptoUtils.readCertificate(certIn));
|
||||
if (cert != null) {
|
||||
bootsig.setCertificate(cert);
|
||||
}
|
||||
if (bootsig.verify(rawImg, signableSize)) {
|
||||
System.err.println("Signature is VALID");
|
||||
@@ -314,4 +320,46 @@ public class SignBoot {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
if (args.length > 0 && "-verify".equals(args[0])) {
|
||||
X509Certificate cert = null;
|
||||
if (args.length >= 2) {
|
||||
// args[1] is the path to a public key certificate
|
||||
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
|
||||
}
|
||||
boolean signed = SignBoot.verifySignature(System.in, cert);
|
||||
System.exit(signed ? 0 : 1);
|
||||
} else if (args.length > 0 && "-sign".equals(args[0])) {
|
||||
X509Certificate cert = null;
|
||||
PrivateKey key = null;
|
||||
String name = "/boot";
|
||||
|
||||
if (args.length >= 3) {
|
||||
cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
|
||||
key = CryptoUtils.readPrivateKey(new FileInputStream(args[2]));
|
||||
}
|
||||
if (args.length == 2) {
|
||||
name = args[1];
|
||||
} else if (args.length >= 4) {
|
||||
name = args[3];
|
||||
}
|
||||
|
||||
boolean result = SignBoot.doSignature(cert, key, System.in, System.out, name);
|
||||
System.exit(result ? 0 : 1);
|
||||
} else {
|
||||
System.err.println(
|
||||
"BootSigner <actions> [args]\n" +
|
||||
"Input from stdin, output to stdout\n" +
|
||||
"\n" +
|
||||
"Actions:\n" +
|
||||
" -verify [x509.pem]\n" +
|
||||
" verify image. cert is optional.\n" +
|
||||
" -sign [x509.pem] [pk8] [name]\n" +
|
||||
" sign image. name and the cert/key pair are optional.\n" +
|
||||
" name should be either /boot (default) or /recovery.\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -17,8 +17,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="@{viewModel.insets.left}"
|
||||
android:paddingRight="@{viewModel.insets.right}"
|
||||
app:consumeSystemWindowsInsets="start|end"
|
||||
app:edgeToEdge="true"
|
||||
app:fitsSystemWindowsInsets="start|end"
|
||||
tools:ignore="RtlHardcoded">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
@@ -34,7 +35,7 @@
|
||||
style="@style/WidgetFoundation.Appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@{viewModel.insets.top}">
|
||||
app:fitsSystemWindowsInsets="top">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/main_toolbar"
|
||||
@@ -72,7 +73,9 @@
|
||||
android:layout_gravity="bottom|center_horizontal"
|
||||
android:layout_marginStart="@dimen/l1"
|
||||
android:layout_marginEnd="@dimen/l1"
|
||||
android:layout_marginBottom="@{(int) @dimen/l1 + viewModel.insets.bottom}"
|
||||
android:layout_marginBottom="@dimen/l1"
|
||||
android:fitsSystemWindows="false"
|
||||
app:layout_fitsSystemWindowsInsets="bottom"
|
||||
tools:layout_marginBottom="64dp">
|
||||
|
||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
||||
@@ -81,6 +84,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@android:color/transparent"
|
||||
android:textStyle="bold"
|
||||
android:fitsSystemWindows="false"
|
||||
android:paddingBottom="0dp"
|
||||
app:fitsSystemWindowsInsets="start|end"
|
||||
app:elevation="0dp"
|
||||
app:itemHorizontalTranslationEnabled="false"
|
||||
app:itemIconTint="@color/color_menu_tint"
|
||||
|
@@ -138,6 +138,7 @@
|
||||
<Button
|
||||
android:id="@+id/grant_btn"
|
||||
style="@style/WidgetFoundation.Button.Text"
|
||||
onTouch="@{viewModel.grantTouchListener}"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
|
@@ -12,32 +12,9 @@
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/dialog_base_outside_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintWidth_max="400dp">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
style="@style/WidgetFoundation.Card.Elevated"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
app:cardElevation="@dimen/margin_generic"
|
||||
app:cardUseCompatPadding="true">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
tools:layout_width="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/dialog_base_start"
|
||||
@@ -148,7 +125,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonIDGAF.isEnabled()}"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:focusable="@{data.buttonIDGAF.isEnabled()}"
|
||||
android:onClick="@{() -> data.buttonIDGAF.clicked()}"
|
||||
android:text="@{data.buttonIDGAF.title}"
|
||||
@@ -165,7 +141,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonNeutral.isEnabled()}"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:focusable="@{data.buttonNeutral.isEnabled()}"
|
||||
android:onClick="@{() -> data.buttonNeutral.clicked()}"
|
||||
android:text="@{data.buttonNeutral.title}"
|
||||
@@ -190,7 +165,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonNegative.isEnabled()}"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:focusable="@{data.buttonNegative.isEnabled()}"
|
||||
android:onClick="@{() -> data.buttonNegative.clicked()}"
|
||||
android:text="@{data.buttonNegative.title}"
|
||||
@@ -207,7 +181,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:clickable="@{data.buttonPositive.isEnabled()}"
|
||||
android:filterTouchesWhenObscured="true"
|
||||
android:focusable="@{data.buttonPositive.isEnabled()}"
|
||||
android:onClick="@{() -> data.buttonPositive.clicked()}"
|
||||
android:text="@{data.buttonPositive.title}"
|
||||
@@ -220,11 +193,4 @@
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</layout>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user