├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── github │ │ └── capntrips │ │ └── kernelflasher │ │ └── IFilesystemService.aidl │ ├── assets │ ├── flash_ak3.sh │ ├── flash_ak3_mkbootfs.sh │ ├── ksuinit │ └── mkbootfs │ ├── java │ └── com │ │ └── github │ │ └── capntrips │ │ └── kernelflasher │ │ ├── FilesystemService.kt │ │ ├── MainActivity.kt │ │ ├── MainListener.kt │ │ ├── common │ │ ├── PartitionUtil.kt │ │ ├── extensions │ │ │ ├── ByteArray.kt │ │ │ └── ExtendedFile.kt │ │ └── types │ │ │ ├── backups │ │ │ └── Backup.kt │ │ │ ├── partitions │ │ │ ├── FsMgrFlags.kt │ │ │ ├── FstabEntry.kt │ │ │ └── Partitions.kt │ │ │ └── room │ │ │ ├── AppDatabase.kt │ │ │ ├── Converters.kt │ │ │ └── updates │ │ │ ├── Update.kt │ │ │ └── UpdateDao.kt │ │ └── ui │ │ ├── components │ │ ├── DataCard.kt │ │ ├── DataRow.kt │ │ ├── DataSet.kt │ │ ├── DataValue.kt │ │ ├── DialogButton.kt │ │ ├── FlashButton.kt │ │ ├── FlashList.kt │ │ ├── MyOutlinedButton.kt │ │ ├── SlotCard.kt │ │ └── ViewButton.kt │ │ ├── screens │ │ ├── RefreshableScreen.kt │ │ ├── backups │ │ │ ├── BackupsContent.kt │ │ │ ├── BackupsViewModel.kt │ │ │ └── SlotBackupsContent.kt │ │ ├── error │ │ │ └── ErrorScreen.kt │ │ ├── main │ │ │ ├── MainContent.kt │ │ │ └── MainViewModel.kt │ │ ├── reboot │ │ │ ├── RebootContent.kt │ │ │ └── RebootViewModel.kt │ │ ├── slot │ │ │ ├── SlotContent.kt │ │ │ ├── SlotFlashContent.kt │ │ │ └── SlotViewModel.kt │ │ └── updates │ │ │ ├── UpdatesAddContent.kt │ │ │ ├── UpdatesChangelogContent.kt │ │ │ ├── UpdatesContent.kt │ │ │ ├── UpdatesUrlState.kt │ │ │ ├── UpdatesViewContent.kt │ │ │ └── UpdatesViewModel.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ ├── jniLibs │ ├── arm64-v8a │ │ ├── libhttools_static.so │ │ ├── liblptools_static.so │ │ └── libmagiskboot.so │ └── armeabi-v7a │ │ ├── libhttools_static.so │ │ ├── liblptools_static.so │ │ └── libmagiskboot.so │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_splash_animation.xml │ └── ic_splash_foreground.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── values-ja │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-ru │ └── string.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gradle" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Android Build 2 | permissions: 3 | contents: write 4 | on: 5 | workflow_dispatch: 6 | push: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Set up JDK 21 17 | uses: actions/setup-java@v4 18 | with: 19 | distribution: "temurin" 20 | java-version: 21 21 | 22 | - name: Setup Gradle 23 | uses: gradle/actions/setup-gradle@v4 24 | 25 | - name: Build with Gradle 26 | run: | 27 | chmod +x ./gradlew 28 | ./gradlew assembleRelease 29 | tree app/build/outputs/apk/release 30 | 31 | - uses: r0adkll/sign-android-release@v1.0.4 32 | name: Sign app APK 33 | id: sign_app 34 | with: 35 | releaseDirectory: app/build/outputs/apk/release 36 | signingKeyBase64: ${{ secrets.KEYSTORE }} 37 | alias: ${{ secrets.KEY_ALIAS }} 38 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 39 | keyPassword: ${{ secrets.KEY_PASSWORD }} 40 | env: 41 | BUILD_TOOLS_VERSION: "35.0.0" 42 | 43 | - name: Rename APK 44 | run: | 45 | ls -al app/build/outputs/apk/release 46 | echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}" 47 | cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher.apk 48 | 49 | - name: Upload APK 50 | uses: actions/upload-artifact@v4.3.5 51 | with: 52 | name: KernelFlasher 53 | path: KernelFlasher.apk 54 | 55 | - name: Rename apk 56 | run: | 57 | ls -al 58 | DATE=$(date +'%y.%m.%d') 59 | echo "TAG=$DATE" >> $GITHUB_ENV 60 | 61 | # - name: Upload release 62 | # uses: ncipollo/release-action@v1.14.0 63 | # with: 64 | # allowUpdates: true 65 | # removeArtifacts: true 66 | # name: "1.${{ github.run_number }}.0" 67 | # tag: "v1.${{ github.run_number }}.0" 68 | # body: | 69 | # Note: QMod KernelFlasher, support ksu-lkm 70 | # artifacts: "*.apk" 71 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Android Release 2 | permissions: 3 | contents: write 4 | on: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up JDK 21 16 | uses: actions/setup-java@v4 17 | with: 18 | distribution: "temurin" 19 | java-version: 21 20 | 21 | - name: Setup Gradle 22 | uses: gradle/actions/setup-gradle@v4 23 | 24 | - name: Build with Gradle 25 | run: | 26 | chmod +x ./gradlew 27 | ./gradlew assembleRelease 28 | tree app/build/outputs/apk/release 29 | 30 | - uses: qlenlen/sign-android-release@v2.0.1 31 | name: Sign app APK 32 | id: sign_app 33 | with: 34 | releaseDirectory: app/build/outputs/apk/release 35 | signingKeyBase64: ${{ secrets.KEYSTORE }} 36 | alias: ${{ secrets.KEY_ALIAS }} 37 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 38 | keyPassword: ${{ secrets.KEY_PASSWORD }} 39 | env: 40 | BUILD_TOOLS_VERSION: "35.0.0" 41 | 42 | - name: Rename APK 43 | run: | 44 | ls -al app/build/outputs/apk/release 45 | echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}" 46 | cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher.apk 47 | 48 | - name: Upload APK 49 | uses: actions/upload-artifact@v4.3.5 50 | with: 51 | name: KernelFlasher 52 | path: KernelFlasher.apk 53 | 54 | - name: Rename apk 55 | run: | 56 | ls -al 57 | DATE=$(date +'%y.%m.%d') 58 | echo "TAG=$DATE" >> $GITHUB_ENV 59 | 60 | - name: Upload release 61 | uses: ncipollo/release-action@v1.14.0 62 | with: 63 | allowUpdates: true 64 | removeArtifacts: true 65 | name: "1.${{ github.run_number }}.0" 66 | tag: "v1.${{ github.run_number }}.0" 67 | body: | 68 | Note: QMod KernelFlasher, support ksu-lkm 69 | artifacts: "*.apk" 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /app/release 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 capntrips 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | 21 | 22 | This project bundles lptools (https://github.com/phhusson/vendor_lptools), 23 | which is licensed under the Apache 2.0 license: 24 | 25 | Copyright (C) 2020 Pierre-Hugues Husson 26 | 27 | Licensed under the Apache License, Version 2.0 (the "License"); 28 | you may not use this file except in compliance with the License. 29 | You may obtain a copy of the License at 30 | 31 | http://www.apache.org/licenses/LICENSE-2.0 32 | 33 | Unless required by applicable law or agreed to in writing, software 34 | distributed under the License is distributed on an "AS IS" BASIS, 35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 36 | See the License for the specific language governing permissions and 37 | limitations under the License. 38 | 39 | This project bundles magiskboot (https://github.com/topjohnwu/Magisk), 40 | which is licensed under the GPLv3+ license: 41 | 42 | Copyright (C) 2017-2022 John Wu <@topjohnwu> 43 | 44 | This program is free software: you can redistribute it and/or modify 45 | it under the terms of the GNU General Public License as published by 46 | the Free Software Foundation, either version 3 of the License, or 47 | (at your option) any later version. 48 | 49 | This program is distributed in the hope that it will be useful, 50 | but WITHOUT ANY WARRANTY; without even the implied warranty of 51 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 52 | GNU General Public License for more details. 53 | 54 | You should have received a copy of the GNU General Public License 55 | along with this program. If not, see . 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub release](https://img.shields.io/github/release/qlenlen/KernelFlasher)](https://GitHub.com/qlenlen/KernelFlasher/releases/) 2 | [![Github all releases](https://img.shields.io/github/downloads/qlenlen/KernelFlasher/total)](https://GitHub.com/qlenlen/KernelFlasher/releases/) 3 | 4 | # Kernel Flasher 5 | 6 | Kernel Flasher is an Android app to flash, backup, and restore kernels. 7 | 8 | ## Usage 9 | 10 | `View` a slot and choose to `Flash` an AK3 zip, `Backup` the kernel related partitions, or `Restore` a previous backup. 11 | 12 | There are also options to toggle the mount and map status of `vendor_dlkm` and to save `dmesg` and `logcat`. 13 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.devtools.ksp) 4 | alias(libs.plugins.kotlin.android) 5 | alias(libs.plugins.kotlin.serialization) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | android { 10 | compileSdk 35 11 | 12 | defaultConfig { 13 | applicationId "com.github.capntrips.kernelflasher" 14 | minSdk 33 15 | targetSdk 35 16 | versionCode 28 17 | versionName "1.0.0-alpha28" 18 | 19 | javaCompileOptions { 20 | annotationProcessorOptions { 21 | arguments += [ 22 | "room.schemaLocation": "$projectDir/schemas".toString(), 23 | "room.incremental" : "true" 24 | ] 25 | } 26 | } 27 | ndk { 28 | //noinspection ChromeOsAbiSupport 29 | abiFilters = ['arm64-v8a'] 30 | } 31 | vectorDrawables { 32 | useSupportLibrary true 33 | } 34 | } 35 | buildTypes { 36 | release { 37 | debuggable false 38 | minifyEnabled true 39 | shrinkResources true 40 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 41 | } 42 | } 43 | sourceSets { 44 | main { 45 | jniLibs.srcDirs = ['src/main/jniLibs'] 46 | } 47 | } 48 | buildFeatures { 49 | aidl true 50 | } 51 | compileOptions { 52 | sourceCompatibility JavaVersion.VERSION_17 53 | targetCompatibility JavaVersion.VERSION_17 54 | } 55 | kotlinOptions { 56 | jvmTarget = '17' 57 | } 58 | buildFeatures { 59 | compose true 60 | } 61 | packagingOptions { 62 | resources { 63 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 64 | } 65 | jniLibs { 66 | useLegacyPackaging true 67 | } 68 | } 69 | namespace 'com.github.capntrips.kernelflasher' 70 | } 71 | 72 | dependencies { 73 | implementation(libs.androidx.activity.compose) 74 | implementation(libs.androidx.appcompat) 75 | implementation(libs.androidx.compose.material) 76 | implementation(libs.androidx.compose.material3) 77 | implementation(libs.androidx.compose.foundation) 78 | implementation(libs.androidx.compose.ui) 79 | implementation(libs.androidx.core.ktx) 80 | implementation(libs.androidx.core.splashscreen) 81 | implementation(libs.androidx.lifecycle.runtime.ktx) 82 | implementation(libs.androidx.lifecycle.viewmodel.compose) 83 | implementation(libs.androidx.navigation.compose) 84 | implementation(libs.androidx.room.runtime) 85 | implementation libs.androidx.ui.tooling.preview.android 86 | implementation libs.androidx.animation.core.android 87 | annotationProcessor(libs.androidx.room.compiler) 88 | ksp(libs.androidx.room.compiler) 89 | implementation(libs.libsu.core) 90 | implementation(libs.libsu.io) 91 | implementation(libs.libsu.nio) 92 | implementation(libs.libsu.service) 93 | implementation(libs.material) 94 | implementation(libs.okhttp) 95 | implementation(libs.kotlinx.serialization.json) 96 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -repackageclasses 2 | -allowaccessmodification 3 | -overloadaggressively -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher; 2 | 3 | interface IFilesystemService { 4 | IBinder getFileSystemService(); 5 | } -------------------------------------------------------------------------------- /app/src/main/assets/flash_ak3.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | ## setup for testing: 4 | unzip -p "$Z" tools*/busybox > $F/busybox; 5 | unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary; 6 | ## 7 | 8 | chmod 755 $F/busybox; 9 | $F/busybox chmod 755 $F/update-binary; 10 | $F/busybox chown root:root $F/busybox $F/update-binary; 11 | 12 | TMP=$F/tmp; 13 | 14 | $F/busybox umount $TMP 2>/dev/null; 15 | $F/busybox rm -rf $TMP 2>/dev/null; 16 | $F/busybox mkdir -p $TMP; 17 | 18 | $F/busybox mount -t tmpfs -o noatime tmpfs $TMP; 19 | $F/busybox mount | $F/busybox grep -q " $TMP " || exit 1; 20 | 21 | # update-binary 22 | AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z"; 23 | RC=$?; 24 | 25 | $F/busybox umount $TMP; 26 | $F/busybox rm -rf $TMP; 27 | $F/busybox mount -o ro,remount -t auto /; 28 | $F/busybox rm -f $F/update-binary $F/busybox; 29 | 30 | # work around libsu not cleanly accepting return or exit as last line 31 | safereturn() { return $RC; } 32 | safereturn; 33 | -------------------------------------------------------------------------------- /app/src/main/assets/flash_ak3_mkbootfs.sh: -------------------------------------------------------------------------------- 1 | #!/system/bin/sh 2 | 3 | ## setup for testing: 4 | unzip -p "$Z" tools*/busybox > $F/busybox; 5 | unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary; 6 | ## 7 | 8 | chmod 755 $F/busybox; 9 | $F/busybox chmod 755 $F/update-binary; 10 | $F/busybox chown root:root $F/busybox $F/update-binary; 11 | 12 | TMP=$F/tmp; 13 | 14 | $F/busybox umount $TMP 2>/dev/null; 15 | $F/busybox rm -rf $TMP 2>/dev/null; 16 | $F/busybox mkdir -p $TMP; 17 | 18 | $F/busybox mount -t tmpfs -o noatime tmpfs $TMP; 19 | #$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1; 20 | 21 | PATTERN='\$[Bb][Bb] chmod -R 755 tools bin;'; 22 | sed -i "/$PATTERN/i cp -f \"\$F/mkbootfs\" \$AKHOME/tools;" "$F/update-binary"; 23 | 24 | # update-binary 25 | AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z"; 26 | RC=$?; 27 | 28 | $F/busybox umount $TMP; 29 | $F/busybox rm -rf $TMP; 30 | $F/busybox mount -o ro,remount -t auto /; 31 | $F/busybox rm -f $F/update-binary $F/busybox; 32 | 33 | # work around libsu not cleanly accepting return or exit as last line 34 | safereturn() { return $RC; } 35 | safereturn; -------------------------------------------------------------------------------- /app/src/main/assets/ksuinit: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/assets/ksuinit -------------------------------------------------------------------------------- /app/src/main/assets/mkbootfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/assets/mkbootfs -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher 2 | 3 | import android.content.Intent 4 | import android.os.IBinder 5 | import com.topjohnwu.superuser.ipc.RootService 6 | import com.topjohnwu.superuser.nio.FileSystemManager 7 | 8 | class FilesystemService : RootService() { 9 | inner class FilesystemIPC : IFilesystemService.Stub() { 10 | override fun getFileSystemService(): IBinder { 11 | return FileSystemManager.getService() 12 | } 13 | } 14 | 15 | override fun onBind(intent: Intent): IBinder { 16 | return FilesystemIPC() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher 2 | 3 | internal class MainListener(private val callback: () -> Unit) { 4 | fun resume() { 5 | callback.invoke() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common 2 | 3 | import android.content.Context 4 | import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex 5 | import com.github.capntrips.kernelflasher.common.types.partitions.FstabEntry 6 | import com.topjohnwu.superuser.Shell 7 | import com.topjohnwu.superuser.nio.ExtendedFile 8 | import com.topjohnwu.superuser.nio.FileSystemManager 9 | import kotlinx.serialization.json.Json 10 | import java.io.File 11 | import java.security.DigestOutputStream 12 | import java.security.MessageDigest 13 | 14 | object PartitionUtil { 15 | val PartitionNames = listOf( 16 | "boot", 17 | "dtbo", 18 | "init_boot", 19 | "recovery", 20 | "system_dlkm", 21 | "vbmeta", 22 | "vendor_boot", 23 | "vendor_dlkm", 24 | "vendor_kernel_boot" 25 | ) 26 | 27 | val AvailablePartitions = mutableListOf() 28 | 29 | private var fileSystemManager: FileSystemManager? = null 30 | private var bootParent: File? = null 31 | 32 | fun init(context: Context, fileSystemManager: FileSystemManager) { 33 | this.fileSystemManager = fileSystemManager 34 | val fstabEntry = findPartitionFstabEntry(context, "boot") 35 | if (fstabEntry != null) { 36 | bootParent = File(fstabEntry.blkDevice).parentFile 37 | } 38 | val activeSlotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] 39 | for (partitionName in PartitionNames) { 40 | val blockDevice = findPartitionBlockDevice(context, partitionName, activeSlotSuffix) 41 | if (blockDevice != null && blockDevice.exists()) { 42 | AvailablePartitions.add(partitionName) 43 | } 44 | } 45 | } 46 | 47 | private fun findPartitionFstabEntry(context: Context, partitionName: String): FstabEntry? { 48 | val httools = File(context.filesDir, "httools_static") 49 | val result = Shell.cmd("$httools dump $partitionName").exec().out 50 | if (result.isNotEmpty()) { 51 | return Json.decodeFromString(result[0]) 52 | } 53 | return null 54 | } 55 | 56 | fun isPartitionLogical(context: Context, partitionName: String): Boolean { 57 | return findPartitionFstabEntry(context, partitionName)?.fsMgrFlags?.logical == true 58 | } 59 | 60 | fun findPartitionBlockDevice( 61 | context: Context, 62 | partitionName: String, 63 | slotSuffix: String 64 | ): ExtendedFile? { 65 | var blockDevice: ExtendedFile? = null 66 | val fstabEntry = findPartitionFstabEntry(context, partitionName) 67 | if (fstabEntry != null) { 68 | if (fstabEntry.fsMgrFlags?.logical == true) { 69 | if (fstabEntry.logicalPartitionName == "$partitionName$slotSuffix") { 70 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) 71 | } 72 | } else { 73 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) 74 | if (blockDevice.name != "$partitionName$slotSuffix") { 75 | blockDevice = 76 | fileSystemManager!!.getFile(blockDevice.parentFile, "$partitionName$slotSuffix") 77 | } 78 | } 79 | } 80 | if (blockDevice == null || !blockDevice.exists()) { 81 | val siblingDevice = if (bootParent != null) fileSystemManager!!.getFile( 82 | bootParent!!, 83 | "$partitionName$slotSuffix" 84 | ) else null 85 | val physicalDevice = 86 | fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix") 87 | val logicalDevice = fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix") 88 | if (siblingDevice?.exists() == true) { 89 | blockDevice = physicalDevice 90 | } else if (physicalDevice.exists()) { 91 | blockDevice = physicalDevice 92 | } else if (logicalDevice.exists()) { 93 | blockDevice = logicalDevice 94 | } 95 | } 96 | return blockDevice 97 | } 98 | 99 | @Suppress("unused") 100 | fun partitionAvb(context: Context, partitionName: String): String { 101 | val httools = File(context.filesDir, "httools_static") 102 | val result = Shell.cmd("$httools avb $partitionName").exec().out 103 | return if (result.isNotEmpty()) result[0] else "" 104 | } 105 | 106 | fun flashBlockDevice( 107 | image: ExtendedFile, 108 | blockDevice: ExtendedFile, 109 | hashAlgorithm: String 110 | ): String { 111 | val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt() 112 | val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() 113 | if (partitionSize < imageSize) { 114 | throw Error("Partition ${blockDevice.name} is smaller than image") 115 | } 116 | if (partitionSize > imageSize) { 117 | Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice").exec() 118 | } 119 | val messageDigest = MessageDigest.getInstance(hashAlgorithm) 120 | image.newInputStream().use { inputStream -> 121 | blockDevice.newOutputStream().use { outputStream -> 122 | DigestOutputStream(outputStream, messageDigest).use { digestOutputStream -> 123 | inputStream.copyTo(digestOutputStream) 124 | } 125 | } 126 | } 127 | return messageDigest.digest().toHex() 128 | } 129 | 130 | @Suppress("SameParameterValue") 131 | fun flashLogicalPartition( 132 | context: Context, 133 | image: ExtendedFile, 134 | blockDevice: ExtendedFile, 135 | partitionName: String, 136 | slotSuffix: String, 137 | hashAlgorithm: String, 138 | addMessage: (message: String) -> Unit 139 | ): String { 140 | val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() 141 | val lptools = File(context.filesDir, "lptools_static") 142 | Shell.cmd("$lptools remove ${partitionName}_kf").exec() 143 | if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) { 144 | if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) { 145 | if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) { 146 | val temporaryBlockDevice = 147 | fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf") 148 | val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm) 149 | if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix") 150 | .exec().isSuccess 151 | ) { 152 | return hash 153 | } else { 154 | throw Error("Replacing $partitionName$slotSuffix failed") 155 | } 156 | } else { 157 | throw Error("Remapping ${partitionName}_kf failed") 158 | } 159 | } else { 160 | throw Error("Unmapping ${partitionName}_kf failed") 161 | } 162 | } else { 163 | addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...") 164 | val httools = File(context.filesDir, "httools_static") 165 | if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) { 166 | val verityBlockDevice = blockDevice.parentFile!!.getChildFile("${partitionName}-verity") 167 | if (verityBlockDevice.exists()) { 168 | if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) { 169 | throw Error("Unmapping ${partitionName}-verity failed") 170 | } 171 | } 172 | if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) { 173 | if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)") 174 | .exec().isSuccess 175 | ) { 176 | if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) { 177 | val hash = flashBlockDevice(image, blockDevice, hashAlgorithm) 178 | if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) { 179 | return hash 180 | } else { 181 | throw Error("Mounting $partitionName failed") 182 | } 183 | } else { 184 | throw Error("Remapping $partitionName$slotSuffix failed") 185 | } 186 | } else { 187 | throw Error("Resizing $partitionName$slotSuffix failed") 188 | } 189 | } else { 190 | throw Error("Unmapping $partitionName$slotSuffix failed") 191 | } 192 | } else { 193 | throw Error("Unmounting $partitionName failed") 194 | } 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.extensions 2 | 3 | import kotlin.ByteArray 4 | 5 | object ByteArray { 6 | fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.extensions 2 | 3 | import com.topjohnwu.superuser.nio.ExtendedFile 4 | import java.io.InputStream 5 | import java.io.InputStreamReader 6 | import java.io.OutputStream 7 | import java.nio.charset.Charset 8 | 9 | object ExtendedFile { 10 | private fun ExtendedFile.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = 11 | inputStream().reader(charset) 12 | 13 | private fun ExtendedFile.writeBytes(array: kotlin.ByteArray): Unit = 14 | outputStream().use { it.write(array) } 15 | 16 | fun ExtendedFile.readText(charset: Charset = Charsets.UTF_8): String = 17 | reader(charset).use { it.readText() } 18 | 19 | @Suppress("unused") 20 | fun ExtendedFile.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = 21 | writeBytes(text.toByteArray(charset)) 22 | 23 | fun ExtendedFile.inputStream(): InputStream = newInputStream() 24 | 25 | fun ExtendedFile.outputStream(): OutputStream = newOutputStream() 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.backups 2 | 3 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Backup( 8 | val name: String, 9 | val type: String, 10 | val kernelVersion: String, 11 | val bootSha1: String? = null, 12 | val filename: String? = null, 13 | val hashes: Partitions? = null, 14 | val hashAlgorithm: String? = null 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.partitions 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class FsMgrFlags( 7 | val logical: Boolean = false 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.partitions 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class FstabEntry( 7 | val blkDevice: String, 8 | val mountPoint: String, 9 | val fsType: String, 10 | val logicalPartitionName: String? = null, 11 | val avb: Boolean = false, 12 | val vbmetaPartition: String? = null, 13 | val avbKeys: String? = null, 14 | val fsMgrFlags: FsMgrFlags? = null 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.partitions 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Partitions( 7 | val boot: String? = null, 8 | val dtbo: String? = null, 9 | @Suppress("PropertyName") val init_boot: String? = null, 10 | val recovery: String? = null, 11 | @Suppress("PropertyName") val system_dlkm: String? = null, 12 | val vbmeta: String? = null, 13 | @Suppress("PropertyName") val vendor_boot: String? = null, 14 | @Suppress("PropertyName") val vendor_dlkm: String? = null, 15 | @Suppress("PropertyName") val vendor_kernel_boot: String? = null 16 | ) { 17 | companion object { 18 | fun from(sparseMap: Map) = object { 19 | val map = sparseMap.withDefault { null } 20 | val boot by map 21 | val dtbo by map 22 | val init_boot by map 23 | val recovery by map 24 | val system_dlkm by map 25 | val vbmeta by map 26 | val vendor_boot by map 27 | val vendor_dlkm by map 28 | val vendor_kernel_boot by map 29 | val partitions = Partitions( 30 | boot, 31 | dtbo, 32 | init_boot, 33 | recovery, 34 | system_dlkm, 35 | vbmeta, 36 | vendor_boot, 37 | vendor_dlkm, 38 | vendor_kernel_boot 39 | ) 40 | }.partitions 41 | } 42 | 43 | fun get(partition: String): String? { 44 | return when (partition) { 45 | "boot" -> boot 46 | "dtbo" -> dtbo 47 | "init_boot" -> init_boot 48 | "recovery" -> recovery 49 | "system_dlkm" -> system_dlkm 50 | "vbmeta" -> vbmeta 51 | "vendor_boot" -> vendor_boot 52 | "vendor_dlkm" -> vendor_dlkm 53 | "vendor_kernel_boot" -> vendor_kernel_boot 54 | else -> null 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.github.capntrips.kernelflasher.common.types.room.updates.Update 7 | import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateDao 8 | 9 | @Database(entities = [Update::class], version = 1) 10 | @TypeConverters(Converters::class) 11 | abstract class AppDatabase : RoomDatabase() { 12 | abstract fun updateDao(): UpdateDao 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room 2 | 3 | import androidx.room.TypeConverter 4 | import java.util.Date 5 | 6 | class Converters { 7 | @TypeConverter 8 | fun fromTimestamp(value: Long?): Date? { 9 | return value?.let { Date(it) } 10 | } 11 | 12 | @TypeConverter 13 | fun dateToTimestamp(date: Date?): Long? { 14 | return date?.time 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room.updates 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.Transient 9 | import kotlinx.serialization.descriptors.PrimitiveKind 10 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 11 | import kotlinx.serialization.encoding.Decoder 12 | import kotlinx.serialization.encoding.Encoder 13 | import kotlinx.serialization.json.JsonElement 14 | import kotlinx.serialization.json.JsonObject 15 | import kotlinx.serialization.json.JsonTransformingSerializer 16 | import kotlinx.serialization.json.buildJsonObject 17 | import java.text.SimpleDateFormat 18 | import java.util.Date 19 | import java.util.Locale 20 | 21 | object DateSerializer : KSerializer { 22 | override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) 23 | val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) 24 | override fun serialize(encoder: Encoder, value: Date) = 25 | encoder.encodeString(formatter.format(value)) 26 | 27 | override fun deserialize(decoder: Decoder): Date = formatter.parse(decoder.decodeString())!! 28 | } 29 | 30 | object UpdateSerializer : JsonTransformingSerializer(Update.serializer()) { 31 | override fun transformSerialize(element: JsonElement): JsonElement { 32 | require(element is JsonObject) 33 | return buildJsonObject { 34 | put("kernel", buildJsonObject { 35 | put("name", element["kernelName"]!!) 36 | put("version", element["kernelVersion"]!!) 37 | put("link", element["kernelLink"]!!) 38 | put("changelog_url", element["kernelChangelogUrl"]!!) 39 | put("date", element["kernelDate"]!!) 40 | put("sha1", element["kernelSha1"]!!) 41 | }) 42 | if (element["supportLink"] != null) { 43 | put("support", buildJsonObject { 44 | put("link", element["supportLink"]!!) 45 | }) 46 | } 47 | } 48 | } 49 | 50 | override fun transformDeserialize(element: JsonElement): JsonElement { 51 | require(element is JsonObject) 52 | val kernel = element["kernel"] 53 | val support = element["support"] 54 | require(kernel is JsonObject) 55 | require(support is JsonObject?) 56 | return buildJsonObject { 57 | put("kernelName", kernel["name"]!!) 58 | put("kernelVersion", kernel["version"]!!) 59 | put("kernelLink", kernel["link"]!!) 60 | put("kernelChangelogUrl", kernel["changelog_url"]!!) 61 | put("kernelDate", kernel["date"]!!) 62 | put("kernelSha1", kernel["sha1"]!!) 63 | if (support != null && support["link"] != null) { 64 | put("supportLink", support["link"]!!) 65 | } 66 | } 67 | } 68 | } 69 | 70 | @Entity 71 | @Serializable 72 | data class Update( 73 | @PrimaryKey 74 | @Transient 75 | val id: Int? = null, 76 | @ColumnInfo(name = "update_uri") 77 | @Transient 78 | var updateUri: String? = null, 79 | @ColumnInfo(name = "kernel_name") 80 | var kernelName: String, 81 | @ColumnInfo(name = "kernel_version") 82 | var kernelVersion: String, 83 | @ColumnInfo(name = "kernel_link") 84 | var kernelLink: String, 85 | @ColumnInfo(name = "kernel_changelog_url") 86 | var kernelChangelogUrl: String, 87 | @ColumnInfo(name = "kernel_date") 88 | @Serializable(DateSerializer::class) 89 | var kernelDate: Date, 90 | @ColumnInfo(name = "kernel_sha1") 91 | var kernelSha1: String, 92 | @ColumnInfo(name = "support_link") 93 | var supportLink: String?, 94 | @ColumnInfo(name = "last_updated") 95 | @Transient 96 | var lastUpdated: Date? = null, 97 | ) 98 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room.updates 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | 8 | @Dao 9 | interface UpdateDao { 10 | @Query("""SELECT * FROM "update"""") 11 | fun getAll(): List 12 | 13 | @Query("""SELECT * FROM "update" WHERE id IN (:id)""") 14 | fun load(id: Int): Update 15 | 16 | @Insert 17 | fun insert(update: Update): Long 18 | 19 | @androidx.room.Update 20 | fun update(update: Update) 21 | 22 | @Delete 23 | fun delete(update: Update) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.Card 13 | import androidx.compose.material3.CardDefaults 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.unit.dp 21 | 22 | @Composable 23 | fun DataCard( 24 | title: String, 25 | button: @Composable (() -> Unit)? = null, 26 | content: @Composable (ColumnScope.() -> Unit)? = null 27 | ) { 28 | Card( 29 | modifier = Modifier 30 | .fillMaxWidth(), 31 | shape = RoundedCornerShape(12.dp), 32 | elevation = CardDefaults.cardElevation( 33 | defaultElevation = 8.dp 34 | ), 35 | colors = CardDefaults.cardColors( 36 | containerColor = MaterialTheme.colorScheme.primaryContainer 37 | ) 38 | ) { 39 | Column( 40 | modifier = Modifier.padding(16.dp) 41 | ) { 42 | Row( 43 | modifier = Modifier.fillMaxWidth(), 44 | horizontalArrangement = Arrangement.SpaceBetween, 45 | verticalAlignment = Alignment.CenterVertically 46 | ) { 47 | Text( 48 | text = title, 49 | color = MaterialTheme.colorScheme.onPrimaryContainer, 50 | style = MaterialTheme.typography.titleLarge, 51 | fontWeight = FontWeight.ExtraBold 52 | ) 53 | if (button != null) { 54 | button() 55 | } 56 | } 57 | 58 | if (content != null) { 59 | Spacer(Modifier.height(14.dp)) 60 | content() 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.MutableState 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.layout.layout 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | 20 | @Composable 21 | fun DataRow( 22 | label: String, 23 | value: String, 24 | labelColor: Color = Color.Unspecified, 25 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium, 26 | valueColor: Color = Color.Unspecified, 27 | valueStyle: TextStyle = MaterialTheme.typography.titleSmall, 28 | mutableMaxWidth: MutableState? = null, 29 | clickable: Boolean = false, 30 | ) { 31 | Row { 32 | val labelModifier = if (mutableMaxWidth != null) { 33 | var maxWidth by mutableMaxWidth 34 | Modifier 35 | .padding(bottom = 6.dp) 36 | .layout { measurable, constraints -> 37 | val placeable = measurable.measure(constraints) 38 | maxWidth = maxOf(maxWidth, placeable.width) 39 | layout(width = maxWidth, height = placeable.height) { 40 | placeable.placeRelative(0, 0) 41 | } 42 | } 43 | .alignByBaseline() 44 | } else { 45 | Modifier.alignByBaseline() 46 | } 47 | Text( 48 | modifier = labelModifier.then(Modifier.padding(top = 2.dp)), 49 | text = label, 50 | color = labelColor, 51 | style = labelStyle, 52 | fontSize = 12.5.sp 53 | ) 54 | Spacer(Modifier.width(12.dp)) 55 | DataValue( 56 | value = value, 57 | color = valueColor, 58 | style = valueStyle, 59 | clickable = clickable, 60 | ) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun DataSet( 16 | label: String, 17 | labelColor: Color = Color.Unspecified, 18 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium, 19 | content: @Composable (ColumnScope.() -> Unit) 20 | ) { 21 | Text( 22 | text = label, 23 | color = labelColor, 24 | style = labelStyle 25 | ) 26 | Column(Modifier.padding(start = 16.dp)) { 27 | content() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataValue.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.RowScope 6 | import androidx.compose.foundation.text.selection.SelectionContainer 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.text.style.TextOverflow 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.sp 20 | 21 | @Composable 22 | fun RowScope.DataValue( 23 | value: String, 24 | color: Color = Color.Unspecified, 25 | style: TextStyle = MaterialTheme.typography.titleSmall, 26 | clickable: Boolean = false, 27 | ) { 28 | SelectionContainer( 29 | modifier = Modifier.alignByBaseline() 30 | ) { 31 | var clicked by remember { mutableStateOf(false) } 32 | val textModifier = if (clickable) { 33 | Modifier 34 | .clickable { clicked = !clicked } 35 | .alignByBaseline() 36 | } else { 37 | Modifier.alignByBaseline() 38 | } 39 | Text( 40 | modifier = textModifier, 41 | fontSize = 13.5.sp, 42 | text = value, 43 | color = color, 44 | style = style, 45 | maxLines = if (clicked) Int.MAX_VALUE else 1, 46 | overflow = if (clicked) TextOverflow.Visible else TextOverflow.Ellipsis 47 | ) 48 | } 49 | } 50 | 51 | @Preview 52 | @Composable 53 | fun DataValuePreview() { 54 | Row { 55 | DataValue( 56 | value = "Example Value", 57 | color = Color.Black, 58 | style = MaterialTheme.typography.headlineSmall, 59 | clickable = true 60 | ) 61 | } 62 | } 63 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TextButton 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.LayoutDirection 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun DialogButton( 17 | buttonText: String, 18 | onClick: () -> Unit 19 | ) { 20 | TextButton( 21 | modifier = Modifier.padding(0.dp), 22 | shape = RoundedCornerShape(4.0.dp), 23 | contentPadding = PaddingValues( 24 | horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, 25 | vertical = ButtonDefaults.ContentPadding.calculateTopPadding() 26 | ), 27 | onClick = onClick 28 | ) { 29 | Text( 30 | buttonText, 31 | maxLines = 1, 32 | color = MaterialTheme.colorScheme.primary 33 | ) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import android.net.Uri 4 | import android.provider.OpenableColumns 5 | import android.widget.Toast 6 | import androidx.activity.compose.LocalActivity 7 | import androidx.activity.compose.rememberLauncherForActivityResult 8 | import androidx.activity.result.contract.ActivityResultContracts 9 | import androidx.compose.animation.ExperimentalAnimationApi 10 | import androidx.compose.material.ExperimentalMaterialApi 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.unit.ExperimentalUnitApi 17 | import com.github.capntrips.kernelflasher.MainActivity 18 | import kotlinx.serialization.ExperimentalSerializationApi 19 | 20 | @ExperimentalAnimationApi 21 | @ExperimentalMaterialApi 22 | @ExperimentalMaterial3Api 23 | @ExperimentalSerializationApi 24 | @ExperimentalUnitApi 25 | @Composable 26 | fun FlashButton( 27 | buttonText: String, 28 | validExtension: String, 29 | callback: (uri: Uri) -> Unit 30 | ) { 31 | val mainActivity = LocalActivity.current as MainActivity 32 | val result = remember { mutableStateOf(null) } 33 | val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { 34 | result.value = it 35 | if (it == null) { 36 | mainActivity.isAwaitingResult = false 37 | } 38 | } 39 | MyOutlinedButton( 40 | { 41 | mainActivity.isAwaitingResult = true 42 | launcher.launch("*/*") 43 | } 44 | ) { 45 | Text(buttonText) 46 | } 47 | result.value?.let { uri -> 48 | if (mainActivity.isAwaitingResult) { 49 | val contentResolver = mainActivity.contentResolver 50 | val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor -> 51 | val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) 52 | if (nameIndex != -1 && cursor.moveToFirst()) { 53 | cursor.getString(nameIndex) 54 | } else { 55 | null 56 | } 57 | } 58 | 59 | if (fileName != null && fileName.endsWith(validExtension, ignoreCase = true)) { 60 | callback.invoke(uri) 61 | } else { 62 | // Invalid file extension, show an error message or handle it 63 | Toast.makeText(mainActivity.applicationContext, "Invalid file selected!", Toast.LENGTH_LONG) 64 | .show() 65 | } 66 | } 67 | mainActivity.isAwaitingResult = false 68 | } 69 | mainActivity.isAwaitingResult = false 70 | } 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.interaction.collectIsDraggedAsState 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.LazyListState 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.foundation.lazy.rememberLazyListState 14 | import androidx.compose.material3.LocalTextStyle 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.LaunchedEffect 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableIntStateOf 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.composed 25 | import androidx.compose.ui.draw.drawWithContent 26 | import androidx.compose.ui.geometry.CornerRadius 27 | import androidx.compose.ui.geometry.Offset 28 | import androidx.compose.ui.geometry.Size 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.text.font.FontFamily 31 | import androidx.compose.ui.unit.Dp 32 | import androidx.compose.ui.unit.ExperimentalUnitApi 33 | import androidx.compose.ui.unit.TextUnit 34 | import androidx.compose.ui.unit.TextUnitType 35 | import androidx.compose.ui.unit.dp 36 | 37 | @ExperimentalUnitApi 38 | @Composable 39 | fun ColumnScope.FlashList( 40 | cardTitle: String, 41 | output: List, 42 | content: @Composable ColumnScope.() -> Unit 43 | ) { 44 | val listState = rememberLazyListState() 45 | var hasDragged by remember { mutableStateOf(false) } 46 | val isDragged by listState.interactionSource.collectIsDraggedAsState() 47 | if (isDragged) { 48 | hasDragged = true 49 | } 50 | var shouldScroll = false 51 | if (!hasDragged) { 52 | if (listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != null) { 53 | if (listState.layoutInfo.totalItemsCount - listState.layoutInfo.visibleItemsInfo.size > listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index!!) { 54 | shouldScroll = true 55 | } 56 | } 57 | } 58 | LaunchedEffect(shouldScroll) { 59 | listState.animateScrollToItem(output.size) 60 | } 61 | DataCard(cardTitle) 62 | Spacer(Modifier.height(4.dp)) 63 | LazyColumn( 64 | Modifier 65 | .weight(1.0f) 66 | .fillMaxSize() 67 | .scrollbar(listState), 68 | listState 69 | ) { 70 | items(output) { message -> 71 | Text( 72 | message, 73 | style = LocalTextStyle.current.copy( 74 | fontFamily = FontFamily.Monospace, 75 | fontSize = TextUnit(12.0f, TextUnitType.Sp), 76 | lineHeight = TextUnit(18.0f, TextUnitType.Sp) 77 | ) 78 | ) 79 | } 80 | } 81 | content() 82 | } 83 | 84 | // https://stackoverflow.com/a/68056586/434343 85 | fun Modifier.scrollbar( 86 | state: LazyListState, 87 | width: Dp = 6.dp 88 | ): Modifier = composed { 89 | var visibleItemsCountChanged = false 90 | var visibleItemsCount by remember { mutableIntStateOf(state.layoutInfo.visibleItemsInfo.size) } 91 | if (visibleItemsCount != state.layoutInfo.visibleItemsInfo.size) { 92 | visibleItemsCountChanged = true 93 | visibleItemsCount = state.layoutInfo.visibleItemsInfo.size 94 | } 95 | 96 | val hidden = state.layoutInfo.visibleItemsInfo.size == state.layoutInfo.totalItemsCount 97 | val targetAlpha = 98 | if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0.5f else 0f 99 | val delay = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0 else 250 100 | val duration = 101 | if (hidden || visibleItemsCountChanged) 0 else if (state.isScrollInProgress) 150 else 500 102 | 103 | val alpha by animateFloatAsState( 104 | targetValue = targetAlpha, 105 | animationSpec = tween(delayMillis = delay, durationMillis = duration) 106 | ) 107 | 108 | drawWithContent { 109 | drawContent() 110 | 111 | val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index 112 | val needDrawScrollbar = state.isScrollInProgress || visibleItemsCountChanged || alpha > 0.0f 113 | 114 | if (needDrawScrollbar && firstVisibleElementIndex != null) { 115 | val elementHeight = this.size.height / state.layoutInfo.totalItemsCount 116 | val scrollbarOffsetY = firstVisibleElementIndex * elementHeight 117 | val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight 118 | 119 | drawRoundRect( 120 | color = Color.Gray, 121 | topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), 122 | size = Size(width.toPx(), scrollbarHeight), 123 | cornerRadius = CornerRadius(width.toPx(), width.toPx()), 124 | alpha = alpha 125 | ) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/MyOutlinedButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.ButtonDefaults 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.OutlinedButton 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun MyOutlinedButton( 16 | onclick: () -> Unit, 17 | enabled: Boolean = true, 18 | content: @Composable () -> Unit 19 | ) { 20 | OutlinedButton( 21 | modifier = Modifier 22 | .fillMaxWidth() 23 | .padding(horizontal = 4.dp, vertical = 0.dp), 24 | shape = RoundedCornerShape(10.dp), 25 | colors = ButtonDefaults.outlinedButtonColors( 26 | containerColor = MaterialTheme.colorScheme.secondaryContainer, 27 | contentColor = MaterialTheme.colorScheme.onSecondaryContainer 28 | ), 29 | border = BorderStroke( 30 | width = 1.2.dp, 31 | color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f) 32 | ), 33 | enabled = enabled, 34 | onClick = onclick 35 | ) { content() } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.mutableIntStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.text.font.FontFamily 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.navigation.NavController 14 | import com.github.capntrips.kernelflasher.R 15 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel 16 | 17 | @ExperimentalMaterial3Api 18 | @Composable 19 | fun SlotCard( 20 | title: String, 21 | viewModel: SlotViewModel, 22 | navController: NavController, 23 | isSlotScreen: Boolean = false, 24 | showDlkm: Boolean = true, 25 | ) { 26 | DataCard( 27 | title = title, 28 | button = { 29 | if (!isSlotScreen) { 30 | AnimatedVisibility(!viewModel.isRefreshing.value) { 31 | ViewButton { 32 | navController.navigate("slot${viewModel.slotSuffix}") 33 | } 34 | } 35 | } 36 | } 37 | ) { 38 | val cardWidth = remember { mutableIntStateOf(0) } 39 | if (!viewModel.sha1.isNullOrEmpty()) { 40 | DataRow( 41 | label = stringResource(R.string.boot_sha1), 42 | value = viewModel.sha1!!.substring(0, 8), 43 | valueStyle = MaterialTheme.typography.titleSmall.copy( 44 | fontFamily = FontFamily.Monospace, 45 | fontWeight = FontWeight.Medium 46 | ), 47 | mutableMaxWidth = cardWidth 48 | ) 49 | } 50 | AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.bootInfo.kernelVersion != null) { 51 | DataRow( 52 | label = stringResource(R.string.kernel_version), 53 | value = if (viewModel.bootInfo.kernelVersion != null) viewModel.bootInfo.kernelVersion!! else "", 54 | mutableMaxWidth = cardWidth, 55 | clickable = true 56 | ) 57 | } 58 | if (showDlkm && viewModel.hasVendorDlkm) { 59 | var vendorDlkmValue = stringResource(R.string.not_found) 60 | if (viewModel.isVendorDlkmMapped) { 61 | vendorDlkmValue = if (viewModel.isVendorDlkmMounted) { 62 | String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.mounted)) 63 | } else { 64 | String.format( 65 | "%s, %s", 66 | stringResource(R.string.exists), 67 | stringResource(R.string.unmounted) 68 | ) 69 | } 70 | } 71 | DataRow(stringResource(R.string.vendor_dlkm), vendorDlkmValue, mutableMaxWidth = cardWidth) 72 | } 73 | DataRow( 74 | label = stringResource(R.string.boot_fmt), 75 | value = viewModel.bootInfo.bootFmt ?: stringResource(R.string.not_found), 76 | mutableMaxWidth = cardWidth 77 | ) 78 | DataRow( 79 | label = if (viewModel.bootInfo.ramdiskLocation == "init_boot.img") stringResource(R.string.init_boot_fmt) else stringResource( 80 | R.string.ramdisk_fmt 81 | ), 82 | value = viewModel.bootInfo.initBootFmt ?: stringResource(R.string.not_found), 83 | mutableMaxWidth = cardWidth 84 | ) 85 | if (!viewModel.isRefreshing.value && viewModel.hasError) { 86 | Row { 87 | DataValue( 88 | value = viewModel.error ?: "", 89 | color = MaterialTheme.colorScheme.error, 90 | style = MaterialTheme.typography.titleSmall, 91 | clickable = true 92 | ) 93 | } 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.LayoutDirection 13 | import androidx.compose.ui.unit.dp 14 | import com.github.capntrips.kernelflasher.R 15 | 16 | @Composable 17 | fun ViewButton( 18 | onClick: () -> Unit 19 | ) { 20 | TextButton( 21 | modifier = Modifier.padding(0.dp), 22 | shape = RoundedCornerShape(4.0.dp), 23 | contentPadding = PaddingValues( 24 | horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, 25 | vertical = ButtonDefaults.ContentPadding.calculateTopPadding() 26 | ), 27 | onClick = onClick 28 | ) { 29 | Text(stringResource(R.string.view), maxLines = 1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.ColumnScope 10 | import androidx.compose.foundation.layout.WindowInsets 11 | import androidx.compose.foundation.layout.WindowInsetsSides 12 | import androidx.compose.foundation.layout.asPaddingValues 13 | import androidx.compose.foundation.layout.fillMaxSize 14 | import androidx.compose.foundation.layout.fillMaxWidth 15 | import androidx.compose.foundation.layout.navigationBars 16 | import androidx.compose.foundation.layout.only 17 | import androidx.compose.foundation.layout.padding 18 | import androidx.compose.foundation.layout.statusBars 19 | import androidx.compose.foundation.rememberScrollState 20 | import androidx.compose.foundation.verticalScroll 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 23 | import androidx.compose.material3.ExperimentalMaterial3Api 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.IconButton 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Scaffold 28 | import androidx.compose.material3.Text 29 | import androidx.compose.material3.pulltorefresh.PullToRefreshBox 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.ui.Alignment 32 | import androidx.compose.ui.Modifier 33 | import androidx.compose.ui.graphics.Brush 34 | import androidx.compose.ui.graphics.Color 35 | import androidx.compose.ui.platform.LocalContext 36 | import androidx.compose.ui.res.stringResource 37 | import androidx.compose.ui.text.SpanStyle 38 | import androidx.compose.ui.text.buildAnnotatedString 39 | import androidx.compose.ui.text.font.FontWeight 40 | import androidx.compose.ui.text.withStyle 41 | import androidx.compose.ui.unit.dp 42 | import androidx.compose.ui.unit.sp 43 | import androidx.navigation.NavController 44 | import com.github.capntrips.kernelflasher.R 45 | import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel 46 | import kotlinx.serialization.ExperimentalSerializationApi 47 | 48 | val colorList: List = listOf( 49 | Color(0xE5E57373), 50 | Color(0xE564B5F6), 51 | Color(0xE54DB6AC), 52 | Color(0xE581C784), 53 | Color(0xE5FFD54F), 54 | Color(0xE5FF8A65), 55 | Color(0xE5A1887F), 56 | Color(0xE590A4AE) 57 | ).shuffled() 58 | 59 | 60 | @ExperimentalMaterial3Api 61 | @ExperimentalSerializationApi 62 | @Composable 63 | fun RefreshableScreen( 64 | viewModel: MainViewModel, 65 | navController: NavController, 66 | swipeEnabled: Boolean = false, 67 | content: @Composable ColumnScope.() -> Unit 68 | ) { 69 | val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues() 70 | val navigationBars = WindowInsets.navigationBars.asPaddingValues() 71 | val context = LocalContext.current 72 | 73 | Scaffold( 74 | topBar = { 75 | Box( 76 | Modifier 77 | .fillMaxWidth() 78 | .padding(statusBar) 79 | ) { 80 | if (navController.previousBackStackEntry != null) { 81 | AnimatedVisibility( 82 | !viewModel.isRefreshing, 83 | enter = fadeIn(), 84 | exit = fadeOut() 85 | ) { 86 | IconButton( 87 | onClick = { navController.popBackStack() }, 88 | modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp) 89 | ) { 90 | Icon( 91 | Icons.AutoMirrored.Filled.ArrowBack, 92 | contentDescription = stringResource(R.string.back), 93 | tint = MaterialTheme.colorScheme.onSurface 94 | ) 95 | } 96 | } 97 | } 98 | Box( 99 | Modifier 100 | .fillMaxWidth() 101 | .padding(vertical = 14.dp, horizontal = 2.dp) 102 | ) { 103 | if (isSystemInDarkTheme()) { 104 | Text( 105 | modifier = Modifier 106 | .align(Alignment.Center) 107 | .padding(bottom = 8.dp), 108 | text = buildAnnotatedString { 109 | withStyle( 110 | style = SpanStyle(brush = Brush.linearGradient(colors = colorList)) 111 | ) { 112 | append("Qkernel Flasher") 113 | } 114 | }, 115 | style = MaterialTheme.typography.headlineMedium, 116 | fontWeight = FontWeight.Medium, 117 | fontSize = 23.5.sp 118 | ) 119 | } else { 120 | Text( 121 | modifier = Modifier 122 | .align(Alignment.Center) 123 | .padding(bottom = 8.dp), 124 | text = "Qkernel Flasher", 125 | style = MaterialTheme.typography.headlineMedium, 126 | fontWeight = FontWeight.Medium, 127 | color = MaterialTheme.colorScheme.onSurface, 128 | fontSize = 23.5.sp 129 | ) 130 | } 131 | } 132 | } 133 | } 134 | ) { paddingValues -> 135 | PullToRefreshBox( 136 | isRefreshing = viewModel.isRefreshing, 137 | onRefresh = { viewModel.refresh(context) }, 138 | modifier = Modifier 139 | .padding(paddingValues) 140 | .fillMaxSize() 141 | ) { 142 | Column( 143 | modifier = Modifier 144 | .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding()) 145 | .fillMaxSize() 146 | .verticalScroll(rememberScrollState()), 147 | content = content 148 | ) 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.backups 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.mutableIntStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.text.font.FontFamily 19 | import androidx.compose.ui.text.font.FontStyle 20 | import androidx.compose.ui.text.font.FontWeight 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.unit.dp 23 | import androidx.navigation.NavController 24 | import com.github.capntrips.kernelflasher.R 25 | import com.github.capntrips.kernelflasher.common.PartitionUtil 26 | import com.github.capntrips.kernelflasher.ui.components.DataCard 27 | import com.github.capntrips.kernelflasher.ui.components.DataRow 28 | import com.github.capntrips.kernelflasher.ui.components.DataSet 29 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 30 | import com.github.capntrips.kernelflasher.ui.components.ViewButton 31 | 32 | @ExperimentalMaterial3Api 33 | @Composable 34 | fun ColumnScope.BackupsContent( 35 | viewModel: BackupsViewModel, 36 | navController: NavController 37 | ) { 38 | val context = LocalContext.current 39 | if (viewModel.currentBackup != null && viewModel.backups.containsKey(viewModel.currentBackup)) { 40 | DataCard(viewModel.currentBackup!!) { 41 | val cardWidth = remember { mutableIntStateOf(0) } 42 | val currentBackup = viewModel.backups.getValue(viewModel.currentBackup!!) 43 | DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth) 44 | DataRow( 45 | stringResource(R.string.kernel_version), 46 | currentBackup.kernelVersion, 47 | mutableMaxWidth = cardWidth, 48 | clickable = true 49 | ) 50 | if (currentBackup.type == "raw") { 51 | DataRow( 52 | label = stringResource(R.string.boot_sha1), 53 | value = currentBackup.bootSha1!!.substring(0, 8), 54 | valueStyle = MaterialTheme.typography.titleSmall.copy( 55 | fontFamily = FontFamily.Monospace, 56 | fontWeight = FontWeight.Medium 57 | ), 58 | mutableMaxWidth = cardWidth 59 | ) 60 | if (currentBackup.hashes != null) { 61 | val hashWidth = remember { mutableIntStateOf(0) } 62 | DataSet(stringResource(R.string.hashes)) { 63 | for (partitionName in PartitionUtil.PartitionNames) { 64 | val hash = currentBackup.hashes.get(partitionName) 65 | if (hash != null) { 66 | DataRow( 67 | label = partitionName, 68 | value = hash.substring(0, 8), 69 | valueStyle = MaterialTheme.typography.titleSmall.copy( 70 | fontFamily = FontFamily.Monospace, 71 | fontWeight = FontWeight.Medium 72 | ), 73 | mutableMaxWidth = hashWidth 74 | ) 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | AnimatedVisibility(!viewModel.isRefreshing) { 82 | Column { 83 | Spacer(Modifier.height(5.dp)) 84 | MyOutlinedButton( 85 | onclick = { viewModel.delete(context) { navController.popBackStack() } } 86 | ) { 87 | Text(stringResource(R.string.delete)) 88 | } 89 | } 90 | } 91 | } else { 92 | DataCard(stringResource(R.string.backups)) 93 | AnimatedVisibility(false) { 94 | Column { 95 | Spacer(Modifier.height(5.dp)) 96 | MyOutlinedButton( 97 | onclick = { } 98 | ) { 99 | Text(stringResource(R.string.migrate)) 100 | } 101 | } 102 | } 103 | if (viewModel.backups.isNotEmpty()) { 104 | for (id in viewModel.backups.keys.sortedByDescending { it }) { 105 | val currentBackup = viewModel.backups[id]!! 106 | Spacer(Modifier.height(16.dp)) 107 | DataCard( 108 | title = id, 109 | button = { 110 | AnimatedVisibility(!viewModel.isRefreshing) { 111 | Column { 112 | ViewButton(onClick = { 113 | navController.navigate("backups/$id") 114 | }) 115 | } 116 | } 117 | } 118 | ) { 119 | val cardWidth = remember { mutableIntStateOf(0) } 120 | if (currentBackup.type == "raw") { 121 | DataRow( 122 | label = stringResource(R.string.boot_sha1), 123 | value = currentBackup.bootSha1!!.substring(0, 8), 124 | valueStyle = MaterialTheme.typography.titleSmall.copy( 125 | fontFamily = FontFamily.Monospace, 126 | fontWeight = FontWeight.Medium 127 | ), 128 | mutableMaxWidth = cardWidth 129 | ) 130 | } 131 | DataRow( 132 | stringResource(R.string.kernel_version), 133 | currentBackup.kernelVersion, 134 | mutableMaxWidth = cardWidth, 135 | clickable = true 136 | ) 137 | } 138 | } 139 | } else { 140 | Spacer(Modifier.height(32.dp)) 141 | Text( 142 | stringResource(R.string.no_backups_found), 143 | modifier = Modifier.fillMaxWidth(), 144 | textAlign = TextAlign.Center, 145 | fontStyle = FontStyle.Italic 146 | ) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.backups 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.compose.runtime.MutableState 8 | import androidx.compose.runtime.mutableStateListOf 9 | import androidx.compose.runtime.mutableStateMapOf 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.snapshots.SnapshotStateList 12 | import androidx.compose.runtime.snapshots.SnapshotStateMap 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.viewModelScope 15 | import androidx.navigation.NavController 16 | import com.github.capntrips.kernelflasher.common.PartitionUtil 17 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream 18 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.readText 19 | import com.github.capntrips.kernelflasher.common.types.backups.Backup 20 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions 21 | import com.topjohnwu.superuser.Shell 22 | import com.topjohnwu.superuser.nio.ExtendedFile 23 | import com.topjohnwu.superuser.nio.FileSystemManager 24 | import kotlinx.coroutines.Dispatchers 25 | import kotlinx.coroutines.launch 26 | import kotlinx.coroutines.withContext 27 | import kotlinx.serialization.json.Json 28 | import java.io.File 29 | import java.io.FileInputStream 30 | import java.time.LocalDateTime 31 | import java.time.format.DateTimeFormatter 32 | import java.util.Properties 33 | 34 | class BackupsViewModel( 35 | context: Context, 36 | private val fileSystemManager: FileSystemManager, 37 | private val navController: NavController, 38 | private val _isRefreshing: MutableState, 39 | private val _backups: MutableMap 40 | ) : ViewModel() { 41 | companion object { 42 | const val TAG: String = "KernelFlasher/BackupsState" 43 | } 44 | 45 | private val _restoreOutput: SnapshotStateList = mutableStateListOf() 46 | var currentBackup: String? = null 47 | set(value) { 48 | if (value != field) { 49 | if (_backups[value]?.hashes != null) { 50 | PartitionUtil.AvailablePartitions.forEach { partitionName -> 51 | if (_backups[value]!!.hashes!!.get(partitionName) != null) { 52 | _backupPartitions[partitionName] = true 53 | } 54 | } 55 | } 56 | field = value 57 | } 58 | } 59 | var wasRestored: Boolean? = null 60 | private val _backupPartitions: SnapshotStateMap = mutableStateMapOf() 61 | private val hashAlgorithm: String = "SHA-256" 62 | 63 | @Deprecated( 64 | "Backup migration will be removed in the first stable release", 65 | level = DeprecationLevel.WARNING 66 | ) 67 | private var _needsMigration: MutableState = mutableStateOf(false) 68 | 69 | val restoreOutput: List 70 | get() = _restoreOutput 71 | val backupPartitions: MutableMap 72 | get() = _backupPartitions 73 | val isRefreshing: Boolean 74 | get() = _isRefreshing.value 75 | val backups: Map 76 | get() = _backups 77 | 78 | init { 79 | refresh(context) 80 | } 81 | 82 | fun refresh(context: Context) { 83 | val oldDir = context.getExternalFilesDir(null) 84 | val oldBackupsDir = File(oldDir, "backups") 85 | // Deprecated: Backup migration will be removed in the first stable release 86 | _needsMigration.value = oldBackupsDir.exists() && oldBackupsDir.listFiles()?.size!! > 0 87 | @SuppressLint("SdCardPath") 88 | val externalDir = File("/sdcard/KernelFlasher") 89 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 90 | if (backupsDir.exists()) { 91 | val children = backupsDir.listFiles() 92 | if (children != null) { 93 | for (child in children.sortedByDescending { it.name }) { 94 | if (!child.isDirectory) { 95 | continue 96 | } 97 | val jsonFile = child.getChildFile("backup.json") 98 | if (jsonFile.exists()) { 99 | _backups[child.name] = Json.decodeFromString(jsonFile.readText()) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | 106 | private fun launch(block: suspend () -> Unit) { 107 | viewModelScope.launch(Dispatchers.IO) { 108 | _isRefreshing.value = true 109 | try { 110 | block() 111 | } catch (e: Exception) { 112 | withContext(Dispatchers.Main) { 113 | Log.e(TAG, e.message, e) 114 | navController.navigate("error/${e.message}") { 115 | popUpTo("main") 116 | } 117 | } 118 | } 119 | _isRefreshing.value = false 120 | } 121 | } 122 | 123 | @Suppress("SameParameterValue") 124 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 125 | Log.d(TAG, message) 126 | if (!shouldThrow) { 127 | viewModelScope.launch(Dispatchers.Main) { 128 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 129 | } 130 | } else { 131 | throw Exception(message) 132 | } 133 | } 134 | 135 | fun clearCurrent() { 136 | currentBackup = null 137 | clearRestore() 138 | } 139 | 140 | private fun addMessage(message: String) { 141 | viewModelScope.launch(Dispatchers.Main) { 142 | _restoreOutput.add(message) 143 | } 144 | } 145 | 146 | @Suppress("FunctionName") 147 | private fun _clearRestore() { 148 | _restoreOutput.clear() 149 | wasRestored = null 150 | } 151 | 152 | private fun clearRestore() { 153 | _clearRestore() 154 | _backupPartitions.clear() 155 | } 156 | 157 | @Suppress("unused") 158 | @SuppressLint("SdCardPath") 159 | fun saveLog(context: Context) { 160 | launch { 161 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 162 | val log = File("/sdcard/Download/restore-log--$now.log") 163 | log.writeText(restoreOutput.joinToString("\n")) 164 | if (log.exists()) { 165 | log(context, "Saved restore log to $log") 166 | } else { 167 | log(context, "Failed to save $log", shouldThrow = true) 168 | } 169 | } 170 | } 171 | 172 | private fun restorePartitions( 173 | context: Context, 174 | source: ExtendedFile, 175 | slotSuffix: String 176 | ): Partitions? { 177 | val partitions = HashMap() 178 | for (partitionName in PartitionUtil.PartitionNames) { 179 | if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) { 180 | val image = source.getChildFile("$partitionName.img") 181 | if (image.exists()) { 182 | val blockDevice = 183 | PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) 184 | if (blockDevice != null && blockDevice.exists()) { 185 | addMessage("Restoring $partitionName") 186 | partitions[partitionName] = 187 | if (PartitionUtil.isPartitionLogical(context, partitionName)) { 188 | PartitionUtil.flashLogicalPartition( 189 | context, 190 | image, 191 | blockDevice, 192 | partitionName, 193 | slotSuffix, 194 | hashAlgorithm 195 | ) { message -> 196 | addMessage(message) 197 | } 198 | } else { 199 | PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) 200 | } 201 | } else { 202 | log(context, "Partition $partitionName was not found", shouldThrow = true) 203 | } 204 | } 205 | } 206 | } 207 | if (partitions.isNotEmpty()) { 208 | return Partitions.from(partitions) 209 | } 210 | return null 211 | } 212 | 213 | fun restore(context: Context, slotSuffix: String) { 214 | launch { 215 | _clearRestore() 216 | @SuppressLint("SdCardPath") 217 | val externalDir = File("/sdcard/KernelFlasher") 218 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 219 | val backupDir = backupsDir.getChildFile(currentBackup!!) 220 | if (!backupDir.exists()) { 221 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 222 | return@launch 223 | } 224 | addMessage("Restoring backup $currentBackup") 225 | val hashes = restorePartitions(context, backupDir, slotSuffix) 226 | if (hashes == null) { 227 | log(context, "No partitions restored", shouldThrow = true) 228 | } 229 | addMessage("Backup $currentBackup restored") 230 | wasRestored = true 231 | } 232 | } 233 | 234 | fun delete(context: Context, callback: () -> Unit) { 235 | launch { 236 | @SuppressLint("SdCardPath") 237 | val externalDir = File("/sdcard/KernelFlasher") 238 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 239 | val backupDir = backupsDir.getChildFile(currentBackup!!) 240 | if (!backupDir.exists()) { 241 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 242 | return@launch 243 | } 244 | backupDir.deleteRecursively() 245 | _backups.remove(currentBackup!!) 246 | withContext(Dispatchers.Main) { 247 | callback.invoke() 248 | } 249 | } 250 | } 251 | 252 | @SuppressLint("SdCardPath") 253 | @Deprecated("Backup migration will be removed in the first stable release") 254 | fun migrate(context: Context) { 255 | launch { 256 | val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher") 257 | if (!externalDir.exists()) { 258 | if (!externalDir.mkdir()) { 259 | log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true) 260 | } 261 | } 262 | val backupsDir = externalDir.getChildFile("backups") 263 | if (!backupsDir.exists()) { 264 | if (!backupsDir.mkdir()) { 265 | log(context, "Failed to create backups dir", shouldThrow = true) 266 | } 267 | } 268 | val oldDir = context.getExternalFilesDir(null) 269 | val oldBackupsDir = File(oldDir, "backups") 270 | if (oldBackupsDir.exists()) { 271 | val indentedJson = Json { prettyPrint = true } 272 | val children = oldBackupsDir.listFiles() 273 | if (children != null) { 274 | for (child in children.sortedByDescending { it.name }) { 275 | if (!child.isDirectory) { 276 | child.delete() 277 | continue 278 | } 279 | val propFile = File(child, "backup.prop") 280 | 281 | @Suppress("BlockingMethodInNonBlockingContext") 282 | val inputStream = FileInputStream(propFile) 283 | val props = Properties() 284 | @Suppress("BlockingMethodInNonBlockingContext") 285 | props.load(inputStream) 286 | 287 | val name = child.name 288 | val type = props.getProperty("type", "raw") 289 | val kernelVersion = props.getProperty("kernel") 290 | val bootSha1 = if (type == "raw") props.getProperty("sha1") else null 291 | val filename = if (type == "ak3") "ak3.zip" else null 292 | propFile.delete() 293 | 294 | val dest = backupsDir.getChildFile(child.name) 295 | Shell.cmd("mv $child $dest").exec() 296 | if (!dest.exists()) { 297 | throw Error("Too slow") 298 | } 299 | val jsonFile = dest.getChildFile("backup.json") 300 | val backup = Backup(name, type, kernelVersion, bootSha1, filename) 301 | jsonFile.outputStream() 302 | .use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } 303 | _backups[name] = backup 304 | } 305 | } 306 | oldBackupsDir.delete() 307 | } 308 | refresh(context) 309 | } 310 | } 311 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.backups 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.offset 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.Checkbox 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.OutlinedButton 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.mutableIntStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.alpha 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.text.font.FontFamily 28 | import androidx.compose.ui.text.font.FontStyle 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.text.style.TextAlign 31 | import androidx.compose.ui.unit.ExperimentalUnitApi 32 | import androidx.compose.ui.unit.dp 33 | import androidx.navigation.NavController 34 | import com.github.capntrips.kernelflasher.R 35 | import com.github.capntrips.kernelflasher.common.PartitionUtil 36 | import com.github.capntrips.kernelflasher.ui.components.DataCard 37 | import com.github.capntrips.kernelflasher.ui.components.DataRow 38 | import com.github.capntrips.kernelflasher.ui.components.DataSet 39 | import com.github.capntrips.kernelflasher.ui.components.FlashList 40 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 41 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 42 | import com.github.capntrips.kernelflasher.ui.components.ViewButton 43 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel 44 | 45 | @ExperimentalMaterial3Api 46 | @ExperimentalUnitApi 47 | @Composable 48 | fun ColumnScope.SlotBackupsContent( 49 | slotViewModel: SlotViewModel, 50 | backupsViewModel: BackupsViewModel, 51 | slotSuffix: String, 52 | navController: NavController 53 | ) { 54 | val context = LocalContext.current 55 | if (!navController.currentDestination!!.route!!.contains("/backups/{backupId}/restore")) { 56 | SlotCard( 57 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), 58 | viewModel = slotViewModel, 59 | navController = navController, 60 | isSlotScreen = true, 61 | showDlkm = false, 62 | ) 63 | Spacer(Modifier.height(16.dp)) 64 | if (backupsViewModel.currentBackup != null && backupsViewModel.backups.containsKey( 65 | backupsViewModel.currentBackup 66 | ) 67 | ) { 68 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) 69 | DataCard(backupsViewModel.currentBackup!!) { 70 | val cardWidth = remember { mutableIntStateOf(0) } 71 | DataRow( 72 | stringResource(R.string.backup_type), 73 | currentBackup.type, 74 | mutableMaxWidth = cardWidth 75 | ) 76 | DataRow( 77 | stringResource(R.string.kernel_version), 78 | currentBackup.kernelVersion, 79 | mutableMaxWidth = cardWidth, 80 | clickable = true 81 | ) 82 | if (currentBackup.type == "raw") { 83 | if (!currentBackup.bootSha1.isNullOrEmpty()) { 84 | DataRow( 85 | label = stringResource(R.string.boot_sha1), 86 | value = currentBackup.bootSha1.substring(0, 8), 87 | valueStyle = MaterialTheme.typography.titleSmall.copy( 88 | fontFamily = FontFamily.Monospace, 89 | fontWeight = FontWeight.Medium 90 | ), 91 | mutableMaxWidth = cardWidth 92 | ) 93 | } 94 | if (currentBackup.hashes != null) { 95 | val hashWidth = remember { mutableIntStateOf(0) } 96 | DataSet(stringResource(R.string.hashes)) { 97 | for (partitionName in PartitionUtil.PartitionNames) { 98 | val hash = currentBackup.hashes.get(partitionName) 99 | if (hash != null) { 100 | DataRow( 101 | label = partitionName, 102 | value = hash.takeIf { it.isNotEmpty() }?.substring(0, 8) ?: "Hash not found!", 103 | valueStyle = MaterialTheme.typography.titleSmall.copy( 104 | fontFamily = FontFamily.Monospace, 105 | fontWeight = FontWeight.Medium 106 | ), 107 | mutableMaxWidth = hashWidth 108 | ) 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | AnimatedVisibility(!slotViewModel.isRefreshing.value) { 116 | Column { 117 | Spacer(Modifier.height(5.dp)) 118 | if (slotViewModel.isActive) { 119 | if (currentBackup.type == "raw") { 120 | MyOutlinedButton( 121 | { 122 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore") 123 | } 124 | ) { 125 | Text(stringResource(R.string.restore)) 126 | } 127 | } else if (currentBackup.type == "ak3") { 128 | MyOutlinedButton( 129 | { 130 | slotViewModel.flashAk3( 131 | context, 132 | backupsViewModel.currentBackup!!, 133 | currentBackup.filename!! 134 | ) 135 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { 136 | popUpTo("slot$slotSuffix") 137 | } 138 | } 139 | ) { 140 | Text(stringResource(R.string.flash)) 141 | } 142 | MyOutlinedButton( 143 | { 144 | slotViewModel.flashAk3_mkbootfs( 145 | context, 146 | backupsViewModel.currentBackup!!, 147 | currentBackup.filename!! 148 | ) 149 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { 150 | popUpTo("slot$slotSuffix") 151 | } 152 | } 153 | ) { 154 | Text(stringResource(R.string.flash_ak3_zip_mkbootfs)) 155 | } 156 | } 157 | } 158 | MyOutlinedButton( 159 | { backupsViewModel.delete(context) { navController.popBackStack() } } 160 | ) { 161 | Text(stringResource(R.string.delete)) 162 | } 163 | } 164 | } 165 | } else { 166 | DataCard(stringResource(R.string.backups)) 167 | val backups = backupsViewModel.backups.filter { 168 | it.value.bootSha1.isNullOrEmpty() || it.value.bootSha1.equals(slotViewModel.sha1) || it.value.type == "ak3" 169 | } 170 | if (backups.isNotEmpty()) { 171 | for (id in backups.keys.sortedByDescending { it }) { 172 | Spacer(Modifier.height(16.dp)) 173 | DataCard( 174 | title = id, 175 | button = { 176 | AnimatedVisibility(!slotViewModel.isRefreshing.value) { 177 | ViewButton(onClick = { 178 | navController.navigate("slot$slotSuffix/backups/$id") 179 | }) 180 | } 181 | } 182 | ) { 183 | DataRow( 184 | stringResource(R.string.kernel_version), 185 | backups[id]!!.kernelVersion, 186 | clickable = true 187 | ) 188 | } 189 | } 190 | } else { 191 | Spacer(Modifier.height(32.dp)) 192 | Text( 193 | stringResource(R.string.no_backups_found), 194 | modifier = Modifier.fillMaxWidth(), 195 | textAlign = TextAlign.Center, 196 | fontStyle = FontStyle.Italic 197 | ) 198 | } 199 | } 200 | } else if (navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/restore")) { 201 | DataCard(stringResource(R.string.restore)) 202 | Spacer(Modifier.height(5.dp)) 203 | val disabledColor = ButtonDefaults.buttonColors( 204 | Color.Transparent, 205 | MaterialTheme.colorScheme.onSurface 206 | ) 207 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) 208 | if (currentBackup.hashes != null) { 209 | for (partitionName in PartitionUtil.PartitionNames) { 210 | val hash = currentBackup.hashes.get(partitionName) 211 | if (hash != null) { 212 | OutlinedButton( 213 | modifier = Modifier 214 | .fillMaxWidth() 215 | .alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), 216 | shape = RoundedCornerShape(4.dp), 217 | colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor, 218 | enabled = backupsViewModel.backupPartitions[partitionName] != null, 219 | onClick = { 220 | backupsViewModel.backupPartitions[partitionName] = 221 | !backupsViewModel.backupPartitions[partitionName]!! 222 | }, 223 | ) { 224 | Box(Modifier.fillMaxWidth()) { 225 | Checkbox( 226 | backupsViewModel.backupPartitions[partitionName] == true, null, 227 | Modifier 228 | .align(Alignment.CenterStart) 229 | .offset(x = -(16.dp)) 230 | ) 231 | Text(partitionName, Modifier.align(Alignment.Center)) 232 | } 233 | } 234 | } 235 | } 236 | } else { 237 | Text( 238 | stringResource(R.string.partition_selection_unavailable), 239 | modifier = Modifier.fillMaxWidth(), 240 | textAlign = TextAlign.Center, 241 | fontStyle = FontStyle.Italic 242 | ) 243 | Spacer(Modifier.height(5.dp)) 244 | } 245 | MyOutlinedButton( 246 | { 247 | backupsViewModel.restore(context, slotSuffix) 248 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") { 249 | popUpTo("slot$slotSuffix") 250 | } 251 | }, 252 | enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none { 253 | currentBackup.hashes.get( 254 | it 255 | ) != null && backupsViewModel.backupPartitions[it] == null 256 | } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty()) 257 | ) { 258 | Text(stringResource(R.string.restore)) 259 | } 260 | } else { 261 | FlashList( 262 | stringResource(R.string.restore), 263 | backupsViewModel.restoreOutput 264 | ) { 265 | AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) { 266 | Column { 267 | if (backupsViewModel.wasRestored != false) { 268 | MyOutlinedButton( 269 | { navController.navigate("reboot") } 270 | ) { 271 | Text(stringResource(R.string.reboot)) 272 | } 273 | } 274 | } 275 | } 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.error 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Warning 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Scaffold 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.dp 21 | import com.github.capntrips.kernelflasher.ui.theme.Orange500 22 | 23 | @ExperimentalMaterial3Api 24 | @Composable 25 | fun ErrorScreen(message: String) { 26 | Scaffold { paddingValues -> 27 | Box( 28 | contentAlignment = Alignment.Center, 29 | modifier = Modifier 30 | .padding(paddingValues) 31 | .fillMaxSize() 32 | ) { 33 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 34 | Icon( 35 | Icons.Filled.Warning, 36 | modifier = Modifier 37 | .width(48.dp) 38 | .height(48.dp), 39 | tint = Orange500, 40 | contentDescription = message 41 | ) 42 | Spacer(Modifier.height(8.dp)) 43 | Text( 44 | message, 45 | modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp), 46 | style = MaterialTheme.typography.titleLarge, 47 | ) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.main 2 | 3 | import android.os.Build 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableIntStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.res.stringResource 16 | import androidx.compose.ui.unit.dp 17 | import androidx.navigation.NavController 18 | import com.github.capntrips.kernelflasher.R 19 | import com.github.capntrips.kernelflasher.ui.components.DataCard 20 | import com.github.capntrips.kernelflasher.ui.components.DataRow 21 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 22 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 23 | import kotlinx.serialization.ExperimentalSerializationApi 24 | 25 | @ExperimentalMaterial3Api 26 | @ExperimentalSerializationApi 27 | @Composable 28 | fun ColumnScope.MainContent( 29 | viewModel: MainViewModel, 30 | navController: NavController 31 | ) { 32 | val context = LocalContext.current 33 | DataCard(title = stringResource(R.string.device)) { 34 | val cardWidth = remember { mutableIntStateOf(0) } 35 | DataRow( 36 | stringResource(R.string.model), 37 | "${Build.MODEL} (${Build.DEVICE})", 38 | mutableMaxWidth = cardWidth 39 | ) 40 | DataRow(stringResource(R.string.build_number), Build.ID, mutableMaxWidth = cardWidth) 41 | DataRow( 42 | stringResource(R.string.kernel_version), 43 | viewModel.kernelVersion, 44 | mutableMaxWidth = cardWidth, 45 | clickable = true 46 | ) 47 | if (viewModel.isAb) { 48 | DataRow( 49 | stringResource(R.string.slot_suffix), 50 | viewModel.slotSuffix, 51 | mutableMaxWidth = cardWidth 52 | ) 53 | } 54 | if (viewModel.susfsVersion != "v0.0.0" && viewModel.susfsVersion != "Invalid") { 55 | DataRow( 56 | stringResource(R.string.susfs_version), 57 | viewModel.susfsVersion, 58 | mutableMaxWidth = cardWidth 59 | ) 60 | } 61 | } 62 | Spacer(Modifier.height(16.dp)) 63 | SlotCard( 64 | title = stringResource(if (viewModel.isAb) R.string.slot_a else R.string.slot), 65 | viewModel = viewModel.slotA, 66 | navController = navController 67 | ) 68 | if (viewModel.isAb && viewModel.slotB?.hasError == false) { 69 | Spacer(Modifier.height(16.dp)) 70 | SlotCard( 71 | title = stringResource(R.string.slot_b), 72 | viewModel = viewModel.slotB, 73 | navController = navController 74 | ) 75 | } 76 | Spacer(Modifier.height(16.dp)) 77 | AnimatedVisibility(!viewModel.isRefreshing) { 78 | MyOutlinedButton( 79 | onclick = { navController.navigate("backups") } 80 | ) { 81 | Text(stringResource(R.string.backups)) 82 | } 83 | } 84 | if (viewModel.hasRamoops) { 85 | MyOutlinedButton( 86 | onclick = { viewModel.saveRamoops(context) } 87 | ) { 88 | Text(stringResource(R.string.save_ramoops)) 89 | } 90 | } 91 | MyOutlinedButton( 92 | onclick = { viewModel.saveDmesg(context) } 93 | ) { 94 | Text(stringResource(R.string.save_dmesg)) 95 | } 96 | MyOutlinedButton( 97 | onclick = { viewModel.saveLogcat(context) } 98 | ) { 99 | Text(stringResource(R.string.save_logcat)) 100 | } 101 | AnimatedVisibility(!viewModel.isRefreshing) { 102 | MyOutlinedButton( 103 | onclick = { navController.navigate("reboot") } 104 | ) { 105 | Text(stringResource(R.string.reboot)) 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.main 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.compose.runtime.MutableState 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import androidx.navigation.NavController 12 | import com.github.capntrips.kernelflasher.common.PartitionUtil 13 | import com.github.capntrips.kernelflasher.common.types.backups.Backup 14 | import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsViewModel 15 | import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootViewModel 16 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel 17 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewModel 18 | import com.topjohnwu.superuser.Shell 19 | import com.topjohnwu.superuser.nio.FileSystemManager 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.launch 22 | import kotlinx.coroutines.withContext 23 | import kotlinx.serialization.ExperimentalSerializationApi 24 | import java.io.File 25 | import java.time.LocalDateTime 26 | import java.time.format.DateTimeFormatter 27 | 28 | @ExperimentalSerializationApi 29 | class MainViewModel( 30 | context: Context, 31 | fileSystemManager: FileSystemManager, 32 | private val navController: NavController 33 | ) : ViewModel() { 34 | companion object { 35 | const val TAG: String = "KernelFlasher/MainViewModel" 36 | } 37 | 38 | val slotSuffix: String 39 | 40 | val kernelVersion: String 41 | val susfsVersion: String 42 | val isAb: Boolean 43 | val slotA: SlotViewModel 44 | val slotB: SlotViewModel? 45 | val backups: BackupsViewModel 46 | val updates: UpdatesViewModel 47 | val reboot: RebootViewModel 48 | val hasRamoops: Boolean 49 | 50 | private val _isRefreshing: MutableState = mutableStateOf(true) 51 | private var _error: String? = null 52 | private var _backups: MutableMap = mutableMapOf() 53 | 54 | val isRefreshing: Boolean 55 | get() = _isRefreshing.value 56 | val hasError: Boolean 57 | get() = _error != null 58 | val error: String 59 | get() = _error!! 60 | 61 | data class UpdateDialogData( 62 | val title: String, 63 | val changelog: List, 64 | val onConfirm: () -> Unit 65 | ) 66 | 67 | init { 68 | PartitionUtil.init(context, fileSystemManager) 69 | kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0] 70 | susfsVersion = runCatching { Shell.cmd("susfsd version").exec().out[0] } 71 | .recoverCatching { Shell.cmd("ksu_susfs show version").exec().out[0] } 72 | .getOrDefault("v0.0.0") 73 | slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] 74 | backups = BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups) 75 | updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing) 76 | reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing) 77 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:bootable/recovery/recovery.cpp;l=320 78 | isAb = slotSuffix.isNotEmpty() 79 | if (isAb) { 80 | val bootA = PartitionUtil.findPartitionBlockDevice(context, "boot", "_a")!! 81 | val bootB = PartitionUtil.findPartitionBlockDevice(context, "boot", "_b")!! 82 | val initBootA = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_a") 83 | val initBootB = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_b") 84 | slotA = SlotViewModel( 85 | context, 86 | fileSystemManager, 87 | navController, 88 | _isRefreshing, 89 | slotSuffix == "_a", 90 | "_a", 91 | bootA, 92 | initBootA, 93 | _backups 94 | ) 95 | slotB = SlotViewModel( 96 | context, 97 | fileSystemManager, 98 | navController, 99 | _isRefreshing, 100 | slotSuffix == "_b", 101 | "_b", 102 | bootB, 103 | initBootB, 104 | _backups 105 | ) 106 | } else { 107 | val boot = PartitionUtil.findPartitionBlockDevice(context, "boot", "")!! 108 | val initBoot = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "") 109 | slotA = SlotViewModel( 110 | context, 111 | fileSystemManager, 112 | navController, 113 | _isRefreshing, 114 | true, 115 | "", 116 | boot, 117 | initBoot, 118 | _backups 119 | ) 120 | if (slotA.hasError) { 121 | _error = slotA.error 122 | } 123 | slotB = null 124 | } 125 | 126 | hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists() 127 | _isRefreshing.value = false 128 | } 129 | 130 | fun refresh(context: Context) { 131 | launch { 132 | slotA.refresh(context) 133 | if (isAb) { 134 | slotB!!.refresh(context) 135 | } 136 | backups.refresh(context) 137 | } 138 | } 139 | 140 | private fun launch(block: suspend () -> Unit) { 141 | viewModelScope.launch { 142 | _isRefreshing.value = true 143 | try { 144 | withContext(Dispatchers.IO) { 145 | block() 146 | } 147 | } catch (e: Exception) { 148 | Log.e(TAG, e.message, e) 149 | navController.navigate("error/${e.message}") { 150 | popUpTo("main") 151 | } 152 | } 153 | _isRefreshing.value = false 154 | } 155 | } 156 | 157 | @Suppress("SameParameterValue") 158 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 159 | Log.d(TAG, message) 160 | if (!shouldThrow) { 161 | viewModelScope.launch(Dispatchers.Main) { 162 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 163 | } 164 | } else { 165 | throw Exception(message) 166 | } 167 | } 168 | 169 | fun saveRamoops(context: Context) { 170 | launch { 171 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 172 | 173 | @SuppressLint("SdCardPath") 174 | val ramoops = File("/sdcard/Download/console-ramoops--$now.log") 175 | Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec() 176 | if (ramoops.exists()) { 177 | log(context, "Saved ramoops to $ramoops") 178 | } else { 179 | log(context, "Failed to save $ramoops", shouldThrow = true) 180 | } 181 | } 182 | } 183 | 184 | fun saveDmesg(context: Context) { 185 | launch { 186 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 187 | 188 | @SuppressLint("SdCardPath") 189 | val dmesg = File("/sdcard/Download/dmesg--$now.log") 190 | Shell.cmd("dmesg > $dmesg").exec() 191 | if (dmesg.exists()) { 192 | log(context, "Saved dmesg to $dmesg") 193 | } else { 194 | log(context, "Failed to save $dmesg", shouldThrow = true) 195 | } 196 | } 197 | } 198 | 199 | fun saveLogcat(context: Context) { 200 | launch { 201 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 202 | 203 | @SuppressLint("SdCardPath") 204 | val logcat = File("/sdcard/Download/logcat--$now.log") 205 | Shell.cmd("logcat -d > $logcat").exec() 206 | if (logcat.exists()) { 207 | log(context, "Saved logcat to $logcat") 208 | } else { 209 | log(context, "Failed to save $logcat", shouldThrow = true) 210 | } 211 | } 212 | } 213 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.reboot 2 | 3 | import android.os.PowerManager 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalContext 8 | import androidx.compose.ui.res.stringResource 9 | import com.github.capntrips.kernelflasher.R 10 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 11 | 12 | @Suppress("UnusedReceiverParameter") 13 | @Composable 14 | fun ColumnScope.RebootContent( 15 | viewModel: RebootViewModel 16 | ) { 17 | val context = LocalContext.current 18 | MyOutlinedButton( 19 | { viewModel.rebootSystem() } 20 | ) { 21 | Text(stringResource(R.string.reboot)) 22 | } 23 | if (context.getSystemService(PowerManager::class.java)?.isRebootingUserspaceSupported == true) { 24 | MyOutlinedButton( 25 | { viewModel.rebootUserspace() } 26 | ) { 27 | Text(stringResource(R.string.reboot_userspace)) 28 | } 29 | } 30 | MyOutlinedButton( 31 | { viewModel.rebootRecovery() } 32 | ) { 33 | Text(stringResource(R.string.reboot_recovery)) 34 | } 35 | MyOutlinedButton( 36 | { viewModel.rebootBootloader() } 37 | ) { 38 | Text(stringResource(R.string.reboot_bootloader)) 39 | } 40 | MyOutlinedButton( 41 | { viewModel.rebootDownload() } 42 | ) { 43 | Text(stringResource(R.string.reboot_download)) 44 | } 45 | MyOutlinedButton( 46 | { viewModel.rebootEdl() } 47 | ) { 48 | Text(stringResource(R.string.reboot_edl)) 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.reboot 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.compose.runtime.MutableState 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.navigation.NavController 9 | import com.topjohnwu.superuser.Shell 10 | import com.topjohnwu.superuser.nio.FileSystemManager 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | 15 | class RebootViewModel( 16 | @Suppress("UNUSED_PARAMETER") ignoredContext: Context, 17 | @Suppress("unused") private val fileSystemManager: FileSystemManager, 18 | private val navController: NavController, 19 | private val _isRefreshing: MutableState 20 | ) : ViewModel() { 21 | companion object { 22 | const val TAG: String = "KernelFlasher/RebootState" 23 | } 24 | 25 | val isRefreshing: Boolean 26 | get() = _isRefreshing.value 27 | 28 | private fun launch(block: suspend () -> Unit) { 29 | viewModelScope.launch(Dispatchers.IO) { 30 | _isRefreshing.value = true 31 | try { 32 | block() 33 | } catch (e: Exception) { 34 | withContext(Dispatchers.Main) { 35 | Log.e(TAG, e.message, e) 36 | navController.navigate("error/${e.message}") { 37 | popUpTo("main") 38 | } 39 | } 40 | } 41 | _isRefreshing.value = false 42 | } 43 | } 44 | 45 | private fun reboot(destination: String = "") { 46 | launch { 47 | // https://github.com/topjohnwu/Magisk/blob/v25.2/app/src/main/java/com/topjohnwu/magisk/ktx/XSU.kt#L11-L15 48 | if (destination == "recovery") { 49 | // https://github.com/topjohnwu/Magisk/pull/5637 50 | Shell.cmd("/system/bin/input keyevent 26").submit() 51 | } 52 | Shell.cmd("/system/bin/svc power reboot $destination || /system/bin/reboot $destination") 53 | .submit() 54 | } 55 | } 56 | 57 | fun rebootSystem() { 58 | reboot() 59 | } 60 | 61 | fun rebootUserspace() { 62 | reboot("userspace") 63 | } 64 | 65 | fun rebootRecovery() { 66 | reboot("recovery") 67 | } 68 | 69 | fun rebootBootloader() { 70 | reboot("bootloader") 71 | } 72 | 73 | fun rebootDownload() { 74 | reboot("download") 75 | } 76 | 77 | fun rebootEdl() { 78 | reboot("edl") 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.slot 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.ExperimentalAnimationApi 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.platform.LocalContext 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.unit.ExperimentalUnitApi 16 | import androidx.compose.ui.unit.dp 17 | import androidx.navigation.NavController 18 | import com.github.capntrips.kernelflasher.R 19 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 20 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 21 | 22 | @ExperimentalAnimationApi 23 | @ExperimentalMaterial3Api 24 | @ExperimentalUnitApi 25 | @Composable 26 | fun ColumnScope.SlotContent( 27 | viewModel: SlotViewModel, 28 | slotSuffix: String, 29 | navController: NavController 30 | ) { 31 | val context = LocalContext.current 32 | SlotCard( 33 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), 34 | viewModel = viewModel, 35 | navController = navController, 36 | isSlotScreen = true 37 | ) 38 | AnimatedVisibility(!viewModel.isRefreshing.value) { 39 | Column { 40 | Spacer(Modifier.height(5.dp)) 41 | MyOutlinedButton( 42 | { 43 | navController.navigate("slot$slotSuffix/flash") 44 | } 45 | ) { 46 | Text(stringResource(R.string.flash)) 47 | } 48 | MyOutlinedButton( 49 | { 50 | viewModel.clearFlash(context) 51 | navController.navigate("slot$slotSuffix/backup") 52 | } 53 | ) { 54 | Text(stringResource(R.string.backup)) 55 | } 56 | MyOutlinedButton( 57 | { 58 | navController.navigate("slot$slotSuffix/backups") 59 | } 60 | ) { 61 | Text(stringResource(R.string.restore)) 62 | } 63 | MyOutlinedButton( 64 | { if (!viewModel.isRefreshing.value) viewModel.getKernel(context) } 65 | ) { 66 | Text(stringResource(R.string.check_kernel_version)) 67 | } 68 | if (viewModel.hasVendorDlkm) { 69 | AnimatedVisibility(!viewModel.isRefreshing.value) { 70 | AnimatedVisibility(viewModel.isVendorDlkmMounted) { 71 | MyOutlinedButton( 72 | { viewModel.unmountVendorDlkm(context) } 73 | ) { 74 | Text(stringResource(R.string.unmount_vendor_dlkm)) 75 | } 76 | } 77 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) { 78 | Column { 79 | MyOutlinedButton( 80 | { viewModel.mountVendorDlkm(context) } 81 | ) { 82 | Text(stringResource(R.string.mount_vendor_dlkm)) 83 | } 84 | MyOutlinedButton( 85 | { viewModel.unmapVendorDlkm(context) } 86 | ) { 87 | Text(stringResource(R.string.unmap_vendor_dlkm)) 88 | } 89 | } 90 | } 91 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) { 92 | MyOutlinedButton( 93 | { viewModel.mapVendorDlkm(context) } 94 | ) { 95 | Text(stringResource(R.string.map_vendor_dlkm)) 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.slot 2 | 3 | import androidx.activity.compose.BackHandler 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.animation.ExperimentalAnimationApi 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.ColumnScope 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.offset 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material.ExperimentalMaterialApi 17 | import androidx.compose.material3.AlertDialog 18 | import androidx.compose.material3.ButtonDefaults 19 | import androidx.compose.material3.Checkbox 20 | import androidx.compose.material3.ExperimentalMaterial3Api 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.OutlinedButton 23 | import androidx.compose.material3.Text 24 | import androidx.compose.runtime.Composable 25 | import androidx.compose.runtime.derivedStateOf 26 | import androidx.compose.runtime.getValue 27 | import androidx.compose.runtime.remember 28 | import androidx.compose.ui.Alignment 29 | import androidx.compose.ui.Modifier 30 | import androidx.compose.ui.draw.alpha 31 | import androidx.compose.ui.graphics.Color 32 | import androidx.compose.ui.platform.LocalContext 33 | import androidx.compose.ui.res.stringResource 34 | import androidx.compose.ui.text.font.FontWeight 35 | import androidx.compose.ui.unit.ExperimentalUnitApi 36 | import androidx.compose.ui.unit.dp 37 | import androidx.navigation.NavController 38 | import com.github.capntrips.kernelflasher.R 39 | import com.github.capntrips.kernelflasher.common.PartitionUtil 40 | import com.github.capntrips.kernelflasher.ui.components.DataCard 41 | import com.github.capntrips.kernelflasher.ui.components.DialogButton 42 | import com.github.capntrips.kernelflasher.ui.components.FlashButton 43 | import com.github.capntrips.kernelflasher.ui.components.FlashList 44 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 45 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 46 | import kotlinx.serialization.ExperimentalSerializationApi 47 | 48 | @ExperimentalAnimationApi 49 | @ExperimentalMaterialApi 50 | @ExperimentalMaterial3Api 51 | @ExperimentalUnitApi 52 | @ExperimentalSerializationApi 53 | @Composable 54 | fun ColumnScope.SlotFlashContent( 55 | viewModel: SlotViewModel, 56 | slotSuffix: String, 57 | navController: NavController 58 | ) { 59 | val context = LocalContext.current 60 | 61 | val isRefreshing by remember { derivedStateOf { viewModel.isRefreshing } } 62 | val currentRoute = navController.currentDestination!!.route.orEmpty() 63 | 64 | BackHandler( 65 | enabled = ((currentRoute.endsWith("/flash/ak3") || 66 | currentRoute.endsWith("/flash/image/flash") || 67 | currentRoute.endsWith("/backup/backup")) && isRefreshing.value) 68 | ) { 69 | 70 | } 71 | 72 | if (!listOf( 73 | "/flash/ak3", 74 | "/flash/image/flash", 75 | "/backup/backup" 76 | ).any { navController.currentDestination!!.route!!.endsWith(it) } 77 | ) { 78 | SlotCard( 79 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), 80 | viewModel = viewModel, 81 | navController = navController, 82 | isSlotScreen = true, 83 | showDlkm = false 84 | ) 85 | Spacer(Modifier.height(16.dp)) 86 | if (navController.currentDestination!!.route!!.endsWith("/flash")) { 87 | DataCard(stringResource(R.string.flash)) 88 | Spacer(Modifier.height(5.dp)) 89 | FlashButton(stringResource(R.string.flash_ak3_zip), "zip", callback = { uri -> 90 | navController.navigate("slot$slotSuffix/flash/ak3") { 91 | popUpTo("slot$slotSuffix") 92 | } 93 | viewModel.flashAk3(context, uri) 94 | }) 95 | FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip", callback = { uri -> 96 | navController.navigate("slot$slotSuffix/flash/ak3") { 97 | popUpTo("slot$slotSuffix") 98 | } 99 | viewModel.flashAk3_mkbootfs(context, uri) 100 | }) 101 | FlashButton(stringResource(R.string.flash_ksu_lkm), "ko", callback = { uri -> 102 | navController.navigate("slot$slotSuffix/flash/image/flash") { 103 | popUpTo("slot$slotSuffix") 104 | } 105 | viewModel.flashKsuDriver(context, uri) 106 | }) 107 | MyOutlinedButton( 108 | onclick = { 109 | navController.navigate("slot$slotSuffix/flash/image") 110 | } 111 | ) { 112 | Text(stringResource(R.string.flash_partition_image)) 113 | } 114 | } else if (navController.currentDestination!!.route!!.endsWith("/flash/image")) { 115 | DataCard(stringResource(R.string.flash_partition_image)) 116 | Spacer(Modifier.height(5.dp)) 117 | for (partitionName in PartitionUtil.AvailablePartitions) { 118 | FlashButton(partitionName, "img", callback = { uri -> 119 | navController.navigate("slot$slotSuffix/flash/image/flash") { 120 | popUpTo("slot$slotSuffix") 121 | } 122 | viewModel.flashImage(context, uri, partitionName) 123 | }) 124 | } 125 | } else if (navController.currentDestination!!.route!!.endsWith("/backup")) { 126 | DataCard(stringResource(R.string.backup)) 127 | Spacer(Modifier.height(5.dp)) 128 | val disabledColor = ButtonDefaults.buttonColors( 129 | Color.Transparent, 130 | MaterialTheme.colorScheme.onSurface 131 | ) 132 | for (partitionName in PartitionUtil.AvailablePartitions) { 133 | OutlinedButton( 134 | modifier = Modifier 135 | .fillMaxWidth() 136 | .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), 137 | shape = RoundedCornerShape(4.dp), 138 | colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor, 139 | onClick = { 140 | viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!! 141 | }, 142 | ) { 143 | Box(Modifier.fillMaxWidth()) { 144 | Checkbox( 145 | viewModel.backupPartitions[partitionName]!!, null, 146 | Modifier 147 | .align(Alignment.CenterStart) 148 | .offset(x = -(16.dp)) 149 | ) 150 | Text(partitionName, Modifier.align(Alignment.Center)) 151 | } 152 | } 153 | } 154 | MyOutlinedButton( 155 | { 156 | viewModel.backup(context) 157 | navController.navigate("slot$slotSuffix/backup/backup") { 158 | popUpTo("slot$slotSuffix") 159 | } 160 | }, 161 | enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty() 162 | ) { 163 | Text(stringResource(R.string.backup)) 164 | } 165 | } 166 | } else { 167 | Text("") 168 | FlashList( 169 | stringResource(if (navController.currentDestination!!.route!!.endsWith("/backup/backup")) R.string.backup else R.string.flash), 170 | if (navController.currentDestination!!.route!!.contains("ak3")) viewModel.uiPrintedOutput else viewModel.flashOutput 171 | ) { 172 | AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.wasFlashSuccess.value != null) { 173 | Column { 174 | if (navController.currentDestination!!.route!!.contains("ak3")) { 175 | MyOutlinedButton( 176 | { viewModel.saveLog(context) } 177 | ) { 178 | if (navController.currentDestination!!.route!!.contains("ak3")) { 179 | Text(stringResource(R.string.save_ak3_log)) 180 | } else if (navController.currentDestination!!.route!!.endsWith("/backup/backup")) { 181 | Text(stringResource(R.string.save_backup_log)) 182 | } else { 183 | Text(stringResource(R.string.save_flash_log)) 184 | } 185 | } 186 | } 187 | if (navController.currentDestination!!.route!!.contains("ak3")) { 188 | AnimatedVisibility(!navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/flash/ak3") && viewModel.wasFlashSuccess.value != false) { 189 | MyOutlinedButton( 190 | { 191 | viewModel.backupZip(context) { 192 | navController.navigate("slot$slotSuffix/backups") { 193 | popUpTo("slot$slotSuffix") 194 | } 195 | } 196 | } 197 | ) { 198 | Text(stringResource(R.string.save_ak3_zip_as_backup)) 199 | } 200 | } 201 | } 202 | if (viewModel.wasFlashSuccess.value == true && viewModel.showCautionDialog == true) { 203 | AlertDialog( 204 | onDismissRequest = { viewModel.hideCautionDialog() }, 205 | title = { 206 | Text( 207 | "CAUTION!", 208 | style = MaterialTheme.typography.titleLarge, 209 | fontWeight = FontWeight.Bold 210 | ) 211 | }, 212 | text = { 213 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { 214 | Text("You have flashed to inactive slot!", fontWeight = FontWeight.Bold) 215 | Text( 216 | "But the active slot is not changed after flashing.", 217 | fontWeight = FontWeight.Bold 218 | ) 219 | Text( 220 | "Change active slot or return to System Updater to complete OTA.", 221 | fontWeight = FontWeight.Bold 222 | ) 223 | Text( 224 | "Do not reboot from here, unless you know what you are doing.", 225 | fontWeight = FontWeight.Bold 226 | ) 227 | } 228 | }, 229 | confirmButton = { 230 | DialogButton( 231 | "CHANGE SLOT" 232 | ) { 233 | viewModel.hideCautionDialog() 234 | viewModel.switchSlot(context) 235 | } 236 | }, 237 | dismissButton = { 238 | DialogButton( 239 | "CANCEL" 240 | ) { 241 | viewModel.hideCautionDialog() 242 | } 243 | }, 244 | modifier = Modifier.padding(16.dp) 245 | ) 246 | } 247 | if (viewModel.wasFlashSuccess.value != false && navController.currentDestination!!.route!!.endsWith( 248 | "/backup/backup" 249 | ) 250 | ) { 251 | MyOutlinedButton( 252 | { navController.popBackStack() } 253 | ) { 254 | Text(stringResource(R.string.back)) 255 | } 256 | } else { 257 | MyOutlinedButton( 258 | { navController.navigate("reboot") } 259 | ) { 260 | Text(stringResource(R.string.reboot)) 261 | } 262 | } 263 | } 264 | } 265 | } 266 | } 267 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.OutlinedTextField 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.runtime.setValue 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import androidx.navigation.NavController 20 | import com.github.capntrips.kernelflasher.R 21 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 22 | import kotlinx.serialization.ExperimentalSerializationApi 23 | 24 | @Suppress("UnusedReceiverParameter") 25 | @ExperimentalMaterial3Api 26 | @ExperimentalSerializationApi 27 | @Composable 28 | fun ColumnScope.UpdatesAddContent( 29 | viewModel: UpdatesViewModel, 30 | navController: NavController 31 | ) { 32 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current 33 | var url by remember { mutableStateOf("") } 34 | OutlinedTextField( 35 | value = url, 36 | onValueChange = { url = it }, 37 | label = { Text(stringResource(R.string.url)) }, 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | ) 41 | Spacer(Modifier.height(5.dp)) 42 | MyOutlinedButton( 43 | { viewModel.add(url) { navController.navigate("updates/view/$it") { popUpTo("updates") } } } 44 | ) { 45 | Text(stringResource(R.string.add)) 46 | } 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.LocalTextStyle 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.font.FontFamily 12 | import androidx.compose.ui.unit.ExperimentalUnitApi 13 | import androidx.compose.ui.unit.TextUnit 14 | import androidx.compose.ui.unit.TextUnitType 15 | import androidx.compose.ui.unit.dp 16 | import androidx.navigation.NavController 17 | import com.github.capntrips.kernelflasher.ui.components.DataCard 18 | import kotlinx.serialization.ExperimentalSerializationApi 19 | 20 | @Suppress("UnusedReceiverParameter") 21 | @ExperimentalMaterial3Api 22 | @ExperimentalSerializationApi 23 | @ExperimentalUnitApi 24 | @Composable 25 | fun ColumnScope.UpdatesChangelogContent( 26 | viewModel: UpdatesViewModel, 27 | @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController 28 | ) { 29 | viewModel.currentUpdate?.let { currentUpdate -> 30 | DataCard(currentUpdate.kernelName) 31 | Spacer(Modifier.height(16.dp)) 32 | Text( 33 | viewModel.changelog!!, 34 | style = LocalTextStyle.current.copy( 35 | fontFamily = FontFamily.Monospace, 36 | fontSize = TextUnit(12.0f, TextUnitType.Sp), 37 | lineHeight = TextUnit(18.0f, TextUnitType.Sp) 38 | ) 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.font.FontStyle 18 | import androidx.compose.ui.unit.dp 19 | import androidx.navigation.NavController 20 | import com.github.capntrips.kernelflasher.R 21 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer 22 | import com.github.capntrips.kernelflasher.ui.components.DataCard 23 | import com.github.capntrips.kernelflasher.ui.components.DataRow 24 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 25 | import com.github.capntrips.kernelflasher.ui.components.ViewButton 26 | import kotlinx.serialization.ExperimentalSerializationApi 27 | 28 | @ExperimentalMaterial3Api 29 | @ExperimentalSerializationApi 30 | @Composable 31 | fun ColumnScope.UpdatesContent( 32 | viewModel: UpdatesViewModel, 33 | navController: NavController 34 | ) { 35 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current 36 | DataCard(stringResource(R.string.updates)) 37 | if (viewModel.updates.isNotEmpty()) { 38 | for (update in viewModel.updates.sortedByDescending { it.kernelDate }) { 39 | Spacer(Modifier.height(16.dp)) 40 | DataCard( 41 | title = update.kernelName, 42 | button = { 43 | AnimatedVisibility(!viewModel.isRefreshing) { 44 | Column { 45 | ViewButton(onClick = { 46 | navController.navigate("updates/view/${update.id}") 47 | }) 48 | } 49 | } 50 | } 51 | ) { 52 | val cardWidth = remember { mutableIntStateOf(0) } 53 | DataRow(stringResource(R.string.version), update.kernelVersion, mutableMaxWidth = cardWidth) 54 | DataRow( 55 | stringResource(R.string.date_released), 56 | DateSerializer.formatter.format(update.kernelDate), 57 | mutableMaxWidth = cardWidth 58 | ) 59 | DataRow( 60 | label = stringResource(R.string.last_updated), 61 | value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!), 62 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 63 | labelStyle = MaterialTheme.typography.labelMedium.copy( 64 | fontStyle = FontStyle.Italic 65 | ), 66 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 67 | valueStyle = MaterialTheme.typography.titleSmall.copy( 68 | fontStyle = FontStyle.Italic 69 | ), 70 | mutableMaxWidth = cardWidth 71 | ) 72 | } 73 | } 74 | } 75 | AnimatedVisibility(!viewModel.isRefreshing) { 76 | Column { 77 | Spacer(Modifier.height(12.dp)) 78 | MyOutlinedButton( 79 | { navController.navigate("updates/add") } 80 | ) { 81 | Text(stringResource(R.string.add)) 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesUrlState.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | @Suppress("unused") 4 | class UpdatesUrlState { 5 | // TODO: validate the url field 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.mutableIntStateOf 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.platform.LocalContext 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.text.font.FontStyle 18 | import androidx.compose.ui.unit.dp 19 | import androidx.navigation.NavController 20 | import com.github.capntrips.kernelflasher.R 21 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer 22 | import com.github.capntrips.kernelflasher.ui.components.DataCard 23 | import com.github.capntrips.kernelflasher.ui.components.DataRow 24 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton 25 | import kotlinx.serialization.ExperimentalSerializationApi 26 | 27 | @ExperimentalMaterial3Api 28 | @ExperimentalSerializationApi 29 | @Composable 30 | fun ColumnScope.UpdatesViewContent( 31 | viewModel: UpdatesViewModel, 32 | navController: NavController 33 | ) { 34 | val context = LocalContext.current 35 | viewModel.currentUpdate?.let { currentUpdate -> 36 | DataCard(currentUpdate.kernelName) { 37 | val cardWidth = remember { mutableIntStateOf(0) } 38 | DataRow( 39 | stringResource(R.string.version), 40 | currentUpdate.kernelVersion, 41 | mutableMaxWidth = cardWidth 42 | ) 43 | DataRow( 44 | stringResource(R.string.date_released), 45 | DateSerializer.formatter.format(currentUpdate.kernelDate), 46 | mutableMaxWidth = cardWidth 47 | ) 48 | DataRow( 49 | label = stringResource(R.string.last_updated), 50 | value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!), 51 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 52 | labelStyle = MaterialTheme.typography.labelMedium.copy( 53 | fontStyle = FontStyle.Italic 54 | ), 55 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 56 | valueStyle = MaterialTheme.typography.titleSmall.copy( 57 | fontStyle = FontStyle.Italic, 58 | ), 59 | mutableMaxWidth = cardWidth 60 | ) 61 | } 62 | AnimatedVisibility(!viewModel.isRefreshing) { 63 | Column { 64 | Spacer(Modifier.height(5.dp)) 65 | MyOutlinedButton( 66 | { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } } 67 | ) { 68 | Text(stringResource(R.string.changelog)) 69 | } 70 | // TODO: add download progress indicator 71 | MyOutlinedButton( 72 | { viewModel.downloadKernel(context) } 73 | ) { 74 | Text(stringResource(R.string.download)) 75 | } 76 | MyOutlinedButton( 77 | { viewModel.update() } 78 | ) { 79 | Text(stringResource(R.string.check_for_updates)) 80 | } 81 | MyOutlinedButton( 82 | { viewModel.delete { navController.popBackStack() } } 83 | ) { 84 | Text(stringResource(R.string.delete)) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import android.content.ContentValues 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.os.Environment 7 | import android.provider.MediaStore 8 | import android.util.Log 9 | import android.widget.Toast 10 | import androidx.compose.runtime.MutableState 11 | import androidx.compose.runtime.mutableStateListOf 12 | import androidx.compose.runtime.snapshots.SnapshotStateList 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.viewModelScope 15 | import androidx.navigation.NavController 16 | import androidx.room.Room 17 | import com.github.capntrips.kernelflasher.common.types.room.AppDatabase 18 | import com.github.capntrips.kernelflasher.common.types.room.updates.Update 19 | import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateSerializer 20 | import com.topjohnwu.superuser.nio.FileSystemManager 21 | import kotlinx.coroutines.Dispatchers 22 | import kotlinx.coroutines.launch 23 | import kotlinx.coroutines.withContext 24 | import kotlinx.serialization.ExperimentalSerializationApi 25 | import kotlinx.serialization.json.Json 26 | import okhttp3.OkHttpClient 27 | import okhttp3.Request 28 | import java.io.IOException 29 | import java.text.SimpleDateFormat 30 | import java.util.Date 31 | import java.util.Locale 32 | import kotlin.io.path.Path 33 | import kotlin.io.path.name 34 | 35 | @ExperimentalSerializationApi 36 | class UpdatesViewModel( 37 | context: Context, 38 | @Suppress("unused") private val fileSystemManager: FileSystemManager, 39 | private val navController: NavController, 40 | private val _isRefreshing: MutableState 41 | ) : ViewModel() { 42 | companion object { 43 | const val TAG: String = "KernelFlasher/UpdatesState" 44 | val lastUpdatedFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) 45 | } 46 | 47 | private val client = OkHttpClient() 48 | private val db = Room.databaseBuilder(context, AppDatabase::class.java, "kernel-flasher").build() 49 | private val updateDao = db.updateDao() 50 | private val _updates: SnapshotStateList = mutableStateListOf() 51 | 52 | var currentUpdate: Update? = null 53 | var changelog: String? = null 54 | 55 | val updates: List 56 | get() = _updates 57 | val isRefreshing: Boolean 58 | get() = _isRefreshing.value 59 | 60 | init { 61 | launch { 62 | val updates = updateDao.getAll() 63 | viewModelScope.launch(Dispatchers.Main) { 64 | _updates.addAll(updates) 65 | } 66 | } 67 | } 68 | 69 | private fun launch(block: suspend () -> Unit) { 70 | viewModelScope.launch(Dispatchers.IO) { 71 | viewModelScope.launch(Dispatchers.Main) { 72 | _isRefreshing.value = true 73 | } 74 | try { 75 | block() 76 | } catch (e: Exception) { 77 | withContext(Dispatchers.Main) { 78 | Log.e(TAG, e.message, e) 79 | navController.navigate("error/${e.message}") { 80 | popUpTo("main") 81 | } 82 | } 83 | } 84 | viewModelScope.launch(Dispatchers.Main) { 85 | _isRefreshing.value = false 86 | } 87 | } 88 | } 89 | 90 | @Suppress("SameParameterValue") 91 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 92 | Log.d(TAG, message) 93 | if (!shouldThrow) { 94 | viewModelScope.launch(Dispatchers.Main) { 95 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 96 | } 97 | } else { 98 | throw Exception(message) 99 | } 100 | } 101 | 102 | fun clearCurrent() { 103 | currentUpdate = null 104 | changelog = null 105 | } 106 | 107 | fun add(url: String, callback: (updateId: Int) -> Unit) { 108 | launch { 109 | val request = Request.Builder() 110 | .url(url) 111 | .build() 112 | 113 | client.newCall(request).execute().use { response -> 114 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 115 | val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string()) 116 | update.updateUri = url 117 | update.lastUpdated = Date() 118 | val updateId = updateDao.insert(update).toInt() 119 | val inserted = updateDao.load(updateId) 120 | withContext(Dispatchers.Main) { 121 | _updates.add(inserted) 122 | callback.invoke(updateId) 123 | } 124 | } 125 | } 126 | } 127 | 128 | fun update() { 129 | launch { 130 | val request = Request.Builder() 131 | .url(currentUpdate!!.updateUri!!) 132 | .build() 133 | 134 | client.newCall(request).execute().use { response -> 135 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 136 | val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string()) 137 | currentUpdate!!.let { 138 | withContext(Dispatchers.Main) { 139 | it.kernelName = update.kernelName 140 | it.kernelVersion = update.kernelVersion 141 | it.kernelLink = update.kernelLink 142 | it.kernelChangelogUrl = update.kernelChangelogUrl 143 | it.kernelDate = update.kernelDate 144 | it.kernelSha1 = update.kernelSha1 145 | it.supportLink = update.supportLink 146 | it.lastUpdated = Date() 147 | viewModelScope.launch(Dispatchers.IO) { 148 | updateDao.update(it) 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | 156 | fun downloadChangelog(callback: () -> Unit) { 157 | launch { 158 | val request = Request.Builder() 159 | .url(currentUpdate!!.kernelChangelogUrl) 160 | .build() 161 | 162 | client.newCall(request).execute().use { response -> 163 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 164 | changelog = response.body!!.string() 165 | withContext(Dispatchers.Main) { 166 | callback.invoke() 167 | } 168 | } 169 | } 170 | } 171 | 172 | private fun insertDownload(context: Context, filename: String): Uri? { 173 | val resolver = context.contentResolver 174 | val values = ContentValues() 175 | values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename) 176 | values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip") 177 | values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) 178 | return resolver.insert(MediaStore.Files.getContentUri("external"), values) 179 | } 180 | 181 | fun downloadKernel(context: Context) { 182 | launch { 183 | val remoteUri = Uri.parse(currentUpdate!!.kernelLink) 184 | val filename = Path(remoteUri.path!!).name 185 | val localUri = insertDownload(context, filename) 186 | localUri!!.let { uri -> 187 | val request = Request.Builder() 188 | .url(remoteUri.toString()) 189 | .build() 190 | 191 | client.newCall(request).execute().use { response -> 192 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 193 | response.body!!.byteStream().use { inputStream -> 194 | context.contentResolver.openOutputStream(uri)!!.use { outputStream -> 195 | inputStream.copyTo(outputStream) 196 | } 197 | } 198 | log(context, "Saved $filename to Downloads") 199 | } 200 | } 201 | } 202 | } 203 | 204 | fun delete(callback: () -> Unit) { 205 | launch { 206 | updateDao.delete(currentUpdate!!) 207 | withContext(Dispatchers.Main) { 208 | _updates.remove(currentUpdate!!) 209 | callback.invoke() 210 | currentUpdate = null 211 | } 212 | } 213 | } 214 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Orange500 = Color(0xFFFF9800) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.darkColorScheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.material3.lightColorScheme 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.platform.LocalContext 11 | 12 | @Composable 13 | fun KernelFlasherTheme( 14 | darkTheme: Boolean = isSystemInDarkTheme(), 15 | dynamicColor: Boolean = true, 16 | content: @Composable () -> Unit 17 | ) { 18 | val colorScheme = when { 19 | dynamicColor -> { 20 | val context = LocalContext.current 21 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 22 | } 23 | 24 | darkTheme -> darkColorScheme() 25 | else -> lightColorScheme() 26 | } 27 | MaterialTheme( 28 | colorScheme = colorScheme, 29 | typography = Typography, 30 | content = content 31 | ) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val Typography = Typography().copy() -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/libhttools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/jniLibs/arm64-v8a/libhttools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/liblptools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/jniLibs/arm64-v8a/liblptools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/libmagiskboot.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libhttools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/jniLibs/armeabi-v7a/libhttools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/liblptools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/jniLibs/armeabi-v7a/liblptools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_splash_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 17 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_splash_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-ja/strings.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | Kernel Flasher 4 | Root 権限が必要です 5 | Root サービスが切断されました 6 | デバイス 7 | モデル 8 | ビルド番号 9 | カーネル名 10 | カーネルバージョン 11 | スロットの接頭辞 12 | スロット 13 | スロット A 14 | スロット B 15 | Boot SHA1 16 | Vendor DLKM 17 | あり 18 | なし 19 | マウント済み 20 | アンマウント済み 21 | 表示 22 | バックアップ 23 | ramoops を保存 24 | dmesg を保存 25 | logcat を保存 26 | 戻る 27 | バックアップ 28 | 更新 29 | フラッシュ 30 | mkbootfsを使用してフラッシュする 31 | AK3 Zip をフラッシュ 32 | mkbootfs を使って AK3 Zip をフラッシュする 33 | パーティションイメージをフラッシュ 34 | " KernelSU LKM Driver" 35 | 復元 36 | カーネルバージョンを確認 37 | Vendor DLKM をマウント 38 | Vendor DLKM をアンマウント 39 | Vendor DLKM をマップ 40 | Vendor DLKM をアンマップ 41 | 移行 42 | バックアップが見つかりません 43 | 削除 44 | 追加 45 | URL 46 | バージョン 47 | リリース日 48 | 最終更新 49 | 更新履歴 50 | 更新を確認 51 | ダウンロード 52 | 再起動 53 | ソフトリブート 54 | リカバリーで再起動 55 | ブートローダーで再起動 56 | ダウンロードモードで再起動 57 | EDL で再起動 58 | AK3 ログを保存 59 | フラッシュログを保存 60 | バックアップログを保存 61 | 復元ログを保存 62 | AK3 Zip をバックアップとして保存 63 | バックアップタイプ 64 | ハッシュ 65 | レガシーバックアップではパーティションを選択できません 66 | boot.img形式 67 | init_boot.img形式 68 | Ramdisk形式 69 | SUSFSバージョン 70 | unsupported 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values-pl/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kernel Flasher 4 | Wymagane uprawnienia Superużytkownika 5 | Usługa Superużytkownika została odłączona 6 | Urządzenie 7 | Model 8 | Kompilacja 9 | Nazwa jądra 10 | Wersja jądra 11 | Przyrostek slotu 12 | Slot 13 | Slot A 14 | Slot B 15 | Boot SHA1 16 | Vendor DLKM 17 | Istnieje 18 | Nie znaleziono 19 | Zamontowany 20 | Niezamontowany 21 | Wybierz 22 | Kopie zapasowe 23 | Zapisz ramoops 24 | Zapisz dmesg 25 | Zapisz logcat 26 | Wstecz 27 | Utwórz kopię zapasową 28 | Aktualizacje 29 | Sflashuj 30 | Sflashuj przy użyciu mkbootfs 31 | Sflashuj archiwum AK3 32 | Flashuj archiwum AK3 za pomocą mkbootfs 33 | Sflashuj obraz partycji 34 | Sflashuj KernelSU LKM Driver 35 | Przywróć 36 | Sprawdź wersję jądra 37 | Zamontuj Vendor DLKM 38 | Odmontuj Vendor DLKM 39 | Migruj 40 | Zmapuj Vendor DLKM 41 | Odmapuj Vendor DLKM 42 | Nie znaleziono kopii zapasowych 43 | Usuń 44 | Dodaj 45 | Adres URL 46 | Wersja 47 | Data publikacji 48 | Ostatnia aktualizacja 49 | Lista zmian 50 | Sprawdź aktualizacje 51 | Pobierz 52 | Uruchom ponownie 53 | Miękki restart 54 | Uruchom ponownie do trybu Recovery 55 | Uruchom ponownie do trybu Bootloader 56 | Uruchom ponownie do trybu Download 57 | Uruchom ponownie do trybu EDL 58 | Zapisz dziennik AK3 59 | Zapisz dziennik Flashowania 60 | Zapisz dziennik kopii zapasowej 61 | Zapisz dziennik przywracania 62 | Zapisz archiwum AK3 jako kopię zapasową 63 | Typ kopii zapasowej 64 | Sumy kontrolne 65 | Wybór partycji niedostępny dla kopii zapasowych starszego formatu 66 | Format boot.img 67 | Format init_boot.img 68 | Format Ramdisk 69 | Wersja SUSFS 70 | unsupported 71 | -------------------------------------------------------------------------------- /app/src/main/res/values-pt-rBR/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kernel Flasher 4 | Root é necessário 5 | Serviço root desconectado 6 | Dispositivo 7 | Modelo 8 | Número da versão 9 | Nome do kernel 10 | Versão do kernel 11 | Sufixo de slot 12 | Slot A 13 | Slot B 14 | Boot SHA1 15 | Vendor DLKM 16 | Existe 17 | Não encontrado 18 | Montado 19 | Desmontado 20 | Visualizar 21 | Backups 22 | Salvar ramoops 23 | Salvar dmesg 24 | Salvar logcat 25 | Voltar 26 | Backup 27 | Atualizações 28 | Flash 29 | Flash using mkbootfs 30 | Flash AK3 ZIP 31 | Flash AK3 Zip using mkbootfs 32 | Flashar imagem de partição 33 | Flash KernelSU LKM Driver 34 | Restaurar 35 | Verificar versão do kernel 36 | Montar Vendor DLKM 37 | Desmontar Vendor DLKM 38 | Mapear Vendor DLKM 39 | Desmapear Vendor DLKM 40 | Migrar 41 | Nenhum backup encontrado 42 | Excluir 43 | Adicionar 44 | URL 45 | Versão 46 | Data de lançamento 47 | Ultima atualização 48 | Registro de alterações 49 | Verificar por atualizações 50 | Baixar 51 | Reiniciar 52 | Reinicialização suave 53 | Reiniciar em modo Recovery 54 | Reiniciar em modo Bootloader 55 | Reiniciar em modo Download 56 | Reiniciar em modo EDL 57 | Salvar registro do AK3 58 | Salvar registro do flash 59 | Salvar registro do backup 60 | Salvar registro da restauração 61 | Salvar AK3 ZIP como backup 62 | Tipo de backup 63 | Hashes 64 | Seleção de partição indisponível para backups antigos 65 | Formato do boot.img 66 | Formato do init_boot.img 67 | Formato do Ramdisk 68 | SUSFS Versão 69 | unsupported 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/string.xml: -------------------------------------------------------------------------------- 1 | 2 | Kernel Flasher 3 | Требуется root-доступ 4 | Служба root отключена 5 | Устройство 6 | Модель 7 | Номер сборки 8 | Kernel Name 9 | Версия ядра 10 | Slot Suffix 11 | Слот 12 | Слот A 13 | Слот B 14 | Boot SHA1 15 | Vendor DLKM 16 | Существует 17 | Не найдено 18 | Подключено 19 | Отключено 20 | Просмотр 21 | Резервные копии 22 | Сохранить ramoops 23 | Сохранить dmesg 24 | Сохранить logcat 25 | Назад 26 | Резервное копирование 27 | Обновления 28 | Прошивка 29 | Прошить помощью mkbootfs 30 | Прошить AK3 Zip 31 | Прошить AK3 Zip с помощью mkbootfs 32 | Прошить образ раздела 33 | Прошивка KernelSU LKM Driver 34 | Восстановить 35 | Проверить версию ядра 36 | Подключить Vendor DLKM 37 | Отключить Vendor DLKM 38 | Сопоставить Vendor DLKM 39 | Отменить сопоставление Vendor DLKM 40 | Мигрировать 41 | Резервные копии не найдены 42 | Удалить 43 | Добавить 44 | URL 45 | Версия 46 | Дата выпуска 47 | Последнее обновление 48 | Список изменений 49 | Проверить обновления 50 | Скачать 51 | Перезагрузить 52 | Soft Reboot 53 | Reboot to Recovery 54 | Reboot to Bootloader 55 | Reboot to Download Mode 56 | Reboot to EDL 57 | Сохранить журнал AK3 58 | Сохранить журнал прошивки 59 | Сохранить журнал резервного копирования 60 | Сохранить журнал восстановления 61 | Сохранить AK3 Zip как резервную копию 62 | Тип резервной копии 63 | Хеши 64 | Выбор раздела недоступен для устаревших резервных копий 65 | boot.img формат 66 | init_boot.img формат 67 | Ramdisk формат 68 | SUSFS Версия 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kernel Flasher 4 | 需要 Root 权限 5 | Root 服务已断开 6 | 设备 7 | 型号 8 | 构建版本 9 | 内核名 10 | 内核版本 11 | 插槽后缀 12 | 插槽 A 13 | 插槽 B 14 | Boot 哈希 15 | Vendor DLKM 16 | 存在 17 | 未找到 18 | 已挂载 19 | 未卸载 20 | 查看 21 | 备份 22 | 保存 ramoops 23 | 保存 dmesg 24 | 保存 logcat 25 | 返回 26 | 备份 27 | 更新 28 | 刷入 29 | 使用 mkbootfs 刷入 30 | 刷入 AK3 压缩包 31 | 刷入 AK3 压缩包 使用 mkbootfs 32 | 刷入分区镜像 33 | 刷入 KernelSU LKM Driver 34 | 恢复 35 | 检查内核版本 36 | 挂载 Vendor DLKM 37 | 卸载 Vendor DLKM 38 | 映射 Vendor DLKM 39 | 取消映射 Vendor DLKM 40 | 迁移 41 | 没有找到备份 42 | 删除 43 | 添加 44 | 链接地址 45 | 版本 46 | 发布日期 47 | 更新日期 48 | 变更日志 49 | 检查更新 50 | 下载 51 | 重启 52 | 软重启 53 | 重启到 Recovery 54 | 重启到 Bootloader 55 | 重启到 Download 56 | 重启到 EDL 57 | 保存 AK3 日志 58 | 保存刷写日志 59 | 保存备份日志 60 | 保存恢复日志 61 | 将 AK3 包作为备份保存 62 | 备份类型 63 | 哈希值 64 | 旧的备份无法选择分区 65 | boot.img 格式 66 | init_boot.img 格式 67 | Ramdisk 格式 68 | SUSFS 版本 69 | 不支持 70 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Kernel Flasher 4 | 需要 Root 授權 5 | Root 服務已斷開 6 | 裝置 7 | 型號 8 | 構建版本 9 | 核心名 10 | 核心版本 11 | 插槽字尾 12 | 插槽 13 | 插槽 A 14 | 插槽 B 15 | Boot 雜湊 16 | Vendor DLKM 17 | 存在 18 | 未找到 19 | 已掛載 20 | 未解除安裝 21 | 檢視 22 | 備份 23 | 儲存 ramoops 24 | 儲存 dmesg 25 | 儲存 logcat 26 | 返回 27 | 備份 28 | 更新 29 | 刷入 30 | 刷入 AK3 壓縮包 31 | 刷入分割槽映象 32 | " KernelSU LKM Driver" 33 | 還原 34 | 檢查核心版本 35 | 掛載 Vendor DLKM 36 | 解除安裝 Vendor DLKM 37 | 對映 Vendor DLKM 38 | 取消對映 Vendor DLKM 39 | 遷移 40 | 沒有找到備份 41 | 刪除 42 | 新增 43 | 連結地址 44 | 版本 45 | 釋出日期 46 | 更新日期 47 | 變更日誌 48 | 檢查更新 49 | 下載 50 | 重啟 51 | 軟重啟 52 | 重啟到 Recovery 53 | 重啟到 Bootloader 54 | 重啟到 Download 55 | 重啟到 EDL 56 | 儲存 AK3 日誌 57 | 儲存刷寫日誌 58 | 儲存備份日誌 59 | 儲存還原日誌 60 | 將 AK3 包作為備份儲存 61 | 備份型別 62 | 雜湊值 63 | 舊的備份無法選擇分割槽 64 | 不支持 65 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kernel Flasher 3 | Root is required 4 | Root service disconnected 5 | Device 6 | Model 7 | Build Number 8 | Kernel Name 9 | Kernel Version 10 | Slot Suffix 11 | Slot 12 | Slot A 13 | Slot B 14 | Boot SHA1 15 | Vendor DLKM 16 | Exists 17 | Not Found 18 | Mounted 19 | Unmounted 20 | View 21 | Backups 22 | Save ramoops 23 | Save dmesg 24 | Save logcat 25 | Back 26 | Backup 27 | Updates 28 | Flash 29 | Flash using mkbootfs 30 | Flash AK3 Zip 31 | Flash AK3 Zip using mkbootfs 32 | Flash Partition Image 33 | Flash KernelSU LKM Driver 34 | Restore 35 | Check Kernel Version 36 | Mount Vendor DLKM 37 | Unmount Vendor DLKM 38 | Map Vendor DLKM 39 | Unmap Vendor DLKM 40 | Migrate 41 | No backups found 42 | Delete 43 | Add 44 | URL 45 | Version 46 | Date Released 47 | Last Updated 48 | Changelog 49 | Check for Updates 50 | Download 51 | Reboot 52 | Soft Reboot 53 | Reboot to Recovery 54 | Reboot to Bootloader 55 | Reboot to Download 56 | Reboot to EDL 57 | Save AK3 Log 58 | Save Flash Log 59 | Save Backup Log 60 | Save Restore Log 61 | Save AK3 Zip as Backup 62 | Backup Type 63 | Hashes 64 | Partition selection unavailable for legacy backups 65 | boot.img Format 66 | init_boot.img Format 67 | Ramdisk Format 68 | SUSFS Version 69 | unsupported 70 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) apply false 3 | alias(libs.plugins.devtools.ksp) apply false 4 | alias(libs.plugins.kotlin.android) apply false 5 | alias(libs.plugins.kotlin.serialization) apply false 6 | alias(libs.plugins.compose.compiler) apply false 7 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.21" 3 | 4 | androidx-activity-compose = "1.10.1" 5 | androidx-appcompat = "1.7.1" 6 | androidx-compose = "1.8.2" 7 | androidx-compose-material3 = "1.3.2" 8 | androidx-core-ktx = "1.16.0" 9 | androidx-core-splashscreen = "1.0.1" 10 | androidx-lifecycle = "2.9.1" 11 | androidx-navigation-compose = "2.9.0" 12 | androidx-room = "2.7.1" 13 | kotlinx-serialization-json = "1.8.1" 14 | libsu = "5.2.1" 15 | material = "1.12.0" 16 | okhttp = "4.12.0" 17 | 18 | android-application = "8.10.1" 19 | devtools-ksp = "2.1.21-2.0.1" 20 | ui-tooling-preview-android = "1.8.2" 21 | animation-core-android = "1.8.2" 22 | 23 | [libraries] 24 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" } 25 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 26 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } 27 | androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose" } 28 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } 29 | androidx-compose-ui = { group = "androidx.compose.ui", name="ui", version.ref = "androidx-compose" } 30 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } 31 | androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" } 32 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } 33 | androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 34 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" } 35 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } 36 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" } 37 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } 38 | libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } 39 | libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } 40 | libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" } 41 | libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } 42 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 43 | okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } 44 | androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "ui-tooling-preview-android" } 45 | androidx-animation-core-android = { group = "androidx.compose.animation", name = "animation-core-android", version.ref = "animation-core-android" } 46 | 47 | [plugins] 48 | android-application = { id = "com.android.application", version.ref = "android-application" } 49 | devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" } 50 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 51 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 52 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 53 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qlenlen/KernelFlasher/8b5e1e6c0366b51ef66a2e7f35f438567aee4c4f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 14 13:36:42 CDT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { setUrl("https://maven.aliyun.com/repository/central") } 4 | maven { setUrl("https://maven.aliyun.com/repository/jcenter") } 5 | maven { setUrl("https://maven.aliyun.com/repository/google") } 6 | maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") } 7 | maven { setUrl("https://maven.aliyun.com/repository/public") } 8 | maven { setUrl("https://jitpack.io") } 9 | google() 10 | mavenCentral() 11 | gradlePluginPortal() 12 | } 13 | } 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | maven { setUrl("https://maven.aliyun.com/repository/central") } 18 | maven { setUrl("https://maven.aliyun.com/repository/jcenter") } 19 | maven { setUrl("https://maven.aliyun.com/repository/google") } 20 | maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") } 21 | maven { setUrl("https://maven.aliyun.com/repository/public") } 22 | maven { setUrl("https://jitpack.io") } 23 | google() 24 | mavenCentral() 25 | gradlePluginPortal() 26 | } 27 | } 28 | rootProject.name = "Kernel Flasher" 29 | include ':app' 30 | --------------------------------------------------------------------------------