mirror of
				https://github.com/topjohnwu/Magisk
				synced 2025-11-03 15:52:30 +01:00 
			
		
		
		
	Compare commits
	
		
			85 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | 
							
								
								
									
										87
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,87 @@
 | 
			
		||||
name: Magisk Build
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [ master ]
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
      - 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
 | 
			
		||||
            !~/.gradle/caches/**/*.lock
 | 
			
		||||
          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
 | 
			
		||||
@@ -15,11 +15,11 @@ Here are some feature highlights:
 | 
			
		||||
 | 
			
		||||
## Downloads
 | 
			
		||||
 | 
			
		||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.2/MagiskManager-v8.0.2.apk)
 | 
			
		||||
[](https://github.com/topjohnwu/Magisk/releases/download/manager-v8.0.3/MagiskManager-v8.0.3.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.1)
 | 
			
		||||
 | 
			
		||||
## Useful Links
 | 
			
		||||
 | 
			
		||||
@@ -58,11 +58,11 @@ For Magisk Manager crashes, record and upload the logcat when the crash occurs.
 | 
			
		||||
	- Windows: Add `C:\Path\To\Android Studio\jre\bin` to environment variable `PATH`
 | 
			
		||||
- 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,12 +22,11 @@ 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")
 | 
			
		||||
            mapOf("room.incremental" to "true")
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -64,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")
 | 
			
		||||
}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
tasks["preBuild"]?.dependsOn(copyUtils)
 | 
			
		||||
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("}")
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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}")
 | 
			
		||||
@@ -124,7 +158,7 @@ dependencies {
 | 
			
		||||
    implementation("com.squareup.moshi:moshi:${vMoshi}")
 | 
			
		||||
    kapt("com.squareup.moshi:moshi-kotlin-codegen:${vMoshi}")
 | 
			
		||||
 | 
			
		||||
    val vRoom = "2.3.0-alpha03"
 | 
			
		||||
    val vRoom = "2.3.0-alpha04"
 | 
			
		||||
    implementation("androidx.room:room-runtime:${vRoom}")
 | 
			
		||||
    implementation("androidx.room:room-ktx:${vRoom}")
 | 
			
		||||
    kapt("androidx.room:room-compiler:${vRoom}")
 | 
			
		||||
@@ -136,7 +170,7 @@ dependencies {
 | 
			
		||||
    implementation("androidx.biometric:biometric:1.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")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.67")
 | 
			
		||||
    api("org.bouncycastle:bcpkix-jdk15on:1.67")
 | 
			
		||||
}
 | 
			
		||||
@@ -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);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
    <application
 | 
			
		||||
        android:icon="@drawable/ic_launcher"
 | 
			
		||||
        android:name="a.e"
 | 
			
		||||
        android:allowBackup="true"
 | 
			
		||||
        android:allowBackup="false"
 | 
			
		||||
        tools:ignore="UnusedAttribute,GoogleAppIndexingWarning">
 | 
			
		||||
 | 
			
		||||
        <!-- Splash -->
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,10 @@ 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()
 | 
			
		||||
 
 | 
			
		||||
@@ -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,6 @@ abstract class BaseUIActivity<VM : BaseViewModel, Binding : ViewDataBinding> :
 | 
			
		||||
            it.setVariable(BR.viewModel, viewModel)
 | 
			
		||||
            it.lifecycleOwner = this
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        ensureInsets()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setAccessibilityDelegate(delegate: View.AccessibilityDelegate?) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,9 @@
 | 
			
		||||
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 {
 | 
			
		||||
interface BaseUIComponent<VM : BaseViewModel> : LifecycleOwner {
 | 
			
		||||
 | 
			
		||||
    val viewRoot: View
 | 
			
		||||
    val viewModel: VM
 | 
			
		||||
@@ -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()
 | 
			
		||||
@@ -65,7 +63,6 @@ abstract class BaseUIFragment<VM : BaseViewModel, Binding : ViewDataBinding> :
 | 
			
		||||
                return true
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
        ensureInsets()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,6 @@ import com.topjohnwu.magisk.ktx.inject
 | 
			
		||||
import com.topjohnwu.magisk.ui.theme.Theme
 | 
			
		||||
import org.xmlpull.v1.XmlPullParser
 | 
			
		||||
import java.io.File
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.io.InputStream
 | 
			
		||||
 | 
			
		||||
object Config : PreferenceModel, DBConfig {
 | 
			
		||||
@@ -159,12 +158,13 @@ object Config : PreferenceModel, DBConfig {
 | 
			
		||||
 | 
			
		||||
    private const val SU_FINGERPRINT = "su_fingerprint"
 | 
			
		||||
 | 
			
		||||
    fun load(pkg: String) {
 | 
			
		||||
        try {
 | 
			
		||||
    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) }
 | 
			
		||||
            }
 | 
			
		||||
        } catch (e: IOException) {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        prefs.edit {
 | 
			
		||||
            // Settings migration
 | 
			
		||||
 
 | 
			
		||||
@@ -16,8 +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"
 | 
			
		||||
    val USER_ID = Process.myUid() / 100000
 | 
			
		||||
 | 
			
		||||
    object Version {
 | 
			
		||||
@@ -58,7 +56,7 @@ object Const {
 | 
			
		||||
    object Key {
 | 
			
		||||
        // intents
 | 
			
		||||
        const val OPEN_SECTION = "section"
 | 
			
		||||
        const val HIDDEN_PKG = "hidden_pkg"
 | 
			
		||||
        const val PREV_PKG = "prev_pkg"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    object Value {
 | 
			
		||||
 
 | 
			
		||||
@@ -48,10 +48,10 @@ open class SplashActivity : Activity() {
 | 
			
		||||
        // Pre-initialize root shell
 | 
			
		||||
        Shell.getShell()
 | 
			
		||||
 | 
			
		||||
        val hiddenPackage = intent.getStringExtra(Const.Key.HIDDEN_PKG)
 | 
			
		||||
        val prevPkg = intent.getStringExtra(Const.Key.PREV_PKG)
 | 
			
		||||
 | 
			
		||||
        Config.load(hiddenPackage ?: APPLICATION_ID)
 | 
			
		||||
        handleRepackage(hiddenPackage)
 | 
			
		||||
        Config.load(prevPkg)
 | 
			
		||||
        handleRepackage(prevPkg)
 | 
			
		||||
        Notifications.setup(this)
 | 
			
		||||
        UpdateCheckService.schedule(this)
 | 
			
		||||
        Shortcuts.setupDynamic(this)
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ 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()
 | 
			
		||||
 | 
			
		||||
@@ -101,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(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ 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.android.parcel.Parcelize
 | 
			
		||||
import kotlinx.parcelize.Parcelize
 | 
			
		||||
import java.text.DateFormat
 | 
			
		||||
import java.util.*
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ object HideAPK {
 | 
			
		||||
    private const val ALPHA = "abcdefghijklmnopqrstuvwxyz"
 | 
			
		||||
    private const val ALPHADOTS = "$ALPHA....."
 | 
			
		||||
    private const val APP_NAME = "Magisk Manager"
 | 
			
		||||
    private const val ANDROID_MANIFEST = "AndroidManifest.xml"
 | 
			
		||||
 | 
			
		||||
    // Some arbitrary limit
 | 
			
		||||
    const val MAX_LABEL_LENGTH = 32
 | 
			
		||||
@@ -71,7 +72,7 @@ object HideAPK {
 | 
			
		||||
    ): 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(APPLICATION_ID to pkg, APP_NAME to label.toString()))
 | 
			
		||||
@@ -123,6 +124,7 @@ object HideAPK {
 | 
			
		||||
            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)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -167,7 +169,7 @@ object HideAPK {
 | 
			
		||||
            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.HIDDEN_PKG, packageName)
 | 
			
		||||
            intent.putExtra(Const.Key.PREV_PKG, packageName)
 | 
			
		||||
            startActivity(intent)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 ->
 | 
			
		||||
@@ -225,7 +231,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 +243,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 +295,7 @@ abstract class MagiskInstallImpl : KoinComponent {
 | 
			
		||||
            console.add("****************************")
 | 
			
		||||
        } catch (e: IOException) {
 | 
			
		||||
            console.add("! Failed to output to $outFile")
 | 
			
		||||
            outFile!!.delete()
 | 
			
		||||
            Timber.e(e)
 | 
			
		||||
            return false
 | 
			
		||||
        }
 | 
			
		||||
@@ -325,7 +345,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 +359,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 +400,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")
 | 
			
		||||
 
 | 
			
		||||
@@ -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 < 30) {
 | 
			
		||||
            // Fallback to file based I/O pre Android 11
 | 
			
		||||
            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()
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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()
 | 
			
		||||
 
 | 
			
		||||
@@ -13,18 +13,17 @@ 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,
 | 
			
		||||
@@ -106,18 +105,16 @@ 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)
 | 
			
		||||
                file.uri.outputStream().bufferedWriter().use { writer ->
 | 
			
		||||
                    logItems.forEach {
 | 
			
		||||
                        writer.write(it)
 | 
			
		||||
                        writer.newLine()
 | 
			
		||||
                    }
 | 
			
		||||
        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)
 | 
			
		||||
                    writer.newLine()
 | 
			
		||||
                }
 | 
			
		||||
                SnackbarEvent(file.toString()).publish()
 | 
			
		||||
            }
 | 
			
		||||
            SnackbarEvent(file.toString()).publish()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {
 | 
			
		||||
        HomeFragmentDirections.actionHomeFragmentToInstallFragment().publish()
 | 
			
		||||
    } else {
 | 
			
		||||
        SnackbarEvent(R.string.no_connection).publish()
 | 
			
		||||
    fun onMagiskPressed() = when (state) {
 | 
			
		||||
        State.LOADED -> withExternalRW {
 | 
			
		||||
            HomeFragmentDirections.actionHomeFragmentToInstallFragment().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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -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()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user