├── .github ├── dependabot.yml └── workflows │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle └── 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 │ │ ├── AppUpdater.kt │ │ ├── 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 │ │ ├── Card.kt │ │ ├── DataCard.kt │ │ ├── DataRow.kt │ │ ├── DataSet.kt │ │ ├── DataValue.kt │ │ ├── DialogButton.kt │ │ ├── FlashButton.kt │ │ ├── FlashList.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-it │ └── strings.xml │ ├── values-ja │ └── strings.xml │ ├── values-night │ └── themes.xml │ ├── values-pl │ └── strings.xml │ ├── values-pt-rBR │ └── strings.xml │ ├── values-ru │ └── strings.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 | target-branch: "allow-errors" 6 | schedule: 7 | interval: "daily" 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Android Build 2 | permissions: 3 | contents: write 4 | on: 5 | workflow_dispatch: 6 | push: 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 # Important: get all tags! 18 | 19 | - name: Get Latest Tag 20 | run: | 21 | latest_tag=$(git describe --tags --abbrev=0) 22 | echo "LATEST_TAG=${latest_tag}" >> $GITHUB_ENV 23 | 24 | - name: Count Commits Since Latest Tag 25 | run: | 26 | # Get the number of commits since the latest tag 27 | commits_since_tag=$(git rev-list ${LATEST_TAG}..HEAD --count) 28 | echo "COMMITS_SINCE_TAG=${commits_since_tag}" >> $GITHUB_ENV 29 | 30 | - name: Generate Version 31 | run: | 32 | # Extract version components 33 | version_base="${LATEST_TAG#v}" 34 | major=$(echo $version_base | cut -d. -f1) 35 | minor=$(echo $version_base | cut -d. -f2) 36 | patch=$(echo $version_base | cut -d. -f3) 37 | 38 | # Increment patch by the number of commits since the last tag 39 | new_patch=$((patch + $COMMITS_SINCE_TAG)) 40 | 41 | # Construct the new version string 42 | new_version="$major.$minor.$new_patch" 43 | 44 | # Calculate the version code (e.g., 1.0.5 -> 10005) 45 | version_code=$((major * 10000 + minor * 100 + new_patch)) 46 | 47 | # Set environment variables 48 | echo "NEW_VERSION_NAME=${new_version}" >> $GITHUB_ENV 49 | echo "NEW_VERSION_CODE=${version_code}" >> $GITHUB_ENV 50 | 51 | - name: Update build.gradle with new version code and version name 52 | run: | 53 | # Update versionCode and versionName in build.gradle 54 | sed -i "s/versionCode [0-9]\+/versionCode ${NEW_VERSION_CODE}/" app/build.gradle 55 | sed -i "s/versionName \"[^\"]*\"/versionName \"${NEW_VERSION_NAME}\"/" app/build.gradle 56 | 57 | - name: Print Version 58 | run: | 59 | echo "Version Name: $NEW_VERSION_NAME" 60 | echo "Version Code: $NEW_VERSION_CODE" 61 | 62 | - name: Set up JDK 21 63 | uses: actions/setup-java@v4 64 | with: 65 | distribution: "temurin" 66 | java-version: 21 67 | 68 | - name: Setup Gradle 69 | uses: gradle/actions/setup-gradle@v4 70 | 71 | - name: Build with Gradle 72 | run: | 73 | chmod +x ./gradlew 74 | ./gradlew assembleRelease 75 | tree app/build/outputs/apk/release 76 | 77 | - uses: r0adkll/sign-android-release@v1.0.4 78 | name: Sign app APK 79 | if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'workflow_dispatch' 80 | id: sign_app 81 | with: 82 | releaseDirectory: app/build/outputs/apk/release 83 | signingKeyBase64: ${{ secrets.KEYSTORE }} 84 | alias: ${{ secrets.KEY_ALIAS }} 85 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 86 | keyPassword: ${{ secrets.KEY_PASSWORD }} 87 | env: 88 | BUILD_TOOLS_VERSION: "35.0.0" 89 | 90 | - name: Rename APK 91 | if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'workflow_dispatch' 92 | run: | 93 | ls -al app/build/outputs/apk/release 94 | echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}" 95 | cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk 96 | 97 | - name: Rename APK 98 | if: github.repository != github.event.pull_request.head.repo.full_name && github.event_name == 'pull_request' 99 | run: | 100 | ls -al app/build/outputs/apk/release 101 | cp ./app/build/outputs/apk/release/app-release-unsigned.apk KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk 102 | 103 | - name: Upload APK 104 | uses: actions/upload-artifact@v4.3.5 105 | with: 106 | name: KernelFlasher_${{ env.NEW_VERSION_CODE }} 107 | path: KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk 108 | 109 | # - name: Rename apk 110 | # run: | 111 | # ls -al 112 | # DATE=$(date +'%y.%m.%d') 113 | # echo "TAG=$DATE" >> $GITHUB_ENV 114 | 115 | # - name: Upload release 116 | # uses: ncipollo/release-action@v1.14.0 117 | # with: 118 | # allowUpdates: true 119 | # removeArtifacts: true 120 | # name: "1.${{ github.run_number }}.0" 121 | # tag: "v1.${{ github.run_number }}.0" 122 | # body: | 123 | # Note: QMod KernelFlasher, support ksu-lkm 124 | # artifacts: "*.apk" 125 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Android Release 2 | permissions: 3 | contents: write 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | increment_major: 8 | description: 'Increment Major Version by?' 9 | required: true 10 | default: 0 11 | type: number 12 | increment_minor: 13 | description: 'Increment Minor Version by?' 14 | required: true 15 | default: 0 16 | type: number 17 | increment_patch: 18 | description: 'Increment Patch Version by?' 19 | required: true 20 | default: 0 21 | type: number 22 | changes_in_release: 23 | description: 'Changes in release' 24 | required: true 25 | default: 'Minor changes' 26 | type: string 27 | 28 | jobs: 29 | build: 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 # Important: get all tags! 37 | 38 | - name: Get Latest Tag 39 | run: | 40 | latest_tag=$(git describe --tags --abbrev=0) 41 | echo "LATEST_TAG=${latest_tag}" >> $GITHUB_ENV 42 | 43 | - name: Generate Version 44 | run: | 45 | # Extract version components 46 | version_base="${LATEST_TAG#v}" 47 | major=$(echo $version_base | cut -d. -f1) 48 | minor=$(echo $version_base | cut -d. -f2) 49 | patch=$(echo $version_base | cut -d. -f3) 50 | 51 | # Increment versions by the specified input values 52 | new_major=$((major + ${{ github.event.inputs.increment_major }})) 53 | if [ ${{ github.event.inputs.increment_major }} -gt 0 ]; then 54 | new_minor="${{ github.event.inputs.increment_minor }}" 55 | new_patch="${{ github.event.inputs.increment_patch }}" 56 | else 57 | new_minor=$((minor + ${{ github.event.inputs.increment_minor }})) 58 | if [ ${{ github.event.inputs.increment_minor }} -gt 0 ]; then 59 | new_patch="${{ github.event.inputs.increment_patch }}" 60 | else 61 | new_patch=$((patch + ${{ github.event.inputs.increment_patch }})) 62 | fi 63 | fi 64 | 65 | # Construct the new version string 66 | new_version="$new_major.$new_minor.$new_patch" 67 | 68 | # Calculate the version code (e.g., 1.0.5 -> 10005) 69 | version_code=$((new_major * 10000 + new_minor * 100 + new_patch)) 70 | 71 | # Set environment variables 72 | echo "NEW_VERSION_NAME=${new_version}" >> $GITHUB_ENV 73 | echo "NEW_VERSION_CODE=${version_code}" >> $GITHUB_ENV 74 | 75 | - name: Update build.gradle with new version code and version name 76 | run: | 77 | # Update versionCode and versionName in build.gradle 78 | sed -i "s/versionCode [0-9]\+/versionCode ${NEW_VERSION_CODE}/" app/build.gradle 79 | sed -i "s/versionName \"[^\"]*\"/versionName \"${NEW_VERSION_NAME}\"/" app/build.gradle 80 | 81 | - name: Print Version 82 | run: | 83 | echo "Version Name: $NEW_VERSION_NAME" 84 | echo "Version Code: $NEW_VERSION_CODE" 85 | 86 | - name: Set up JDK 21 87 | uses: actions/setup-java@v4 88 | with: 89 | distribution: "temurin" 90 | java-version: 21 91 | 92 | - name: Setup Gradle 93 | uses: gradle/actions/setup-gradle@v4 94 | 95 | - name: Build with Gradle 96 | run: | 97 | chmod +x ./gradlew 98 | ./gradlew assembleRelease 99 | tree app/build/outputs/apk/release 100 | 101 | - uses: qlenlen/sign-android-release@v2.0.1 102 | name: Sign app APK 103 | id: sign_app 104 | with: 105 | releaseDirectory: app/build/outputs/apk/release 106 | signingKeyBase64: ${{ secrets.KEYSTORE }} 107 | alias: ${{ secrets.KEY_ALIAS }} 108 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} 109 | keyPassword: ${{ secrets.KEY_PASSWORD }} 110 | env: 111 | BUILD_TOOLS_VERSION: "35.0.0" 112 | 113 | - name: Commit the changes 114 | run: | 115 | # Configure git user 116 | git config user.name "github-actions" 117 | git config user.email "github-actions@users.noreply.github.com" 118 | 119 | # Add modified build.gradle file 120 | git add app/build.gradle 121 | 122 | # Commit the changes 123 | git commit -m "Update versionName and versionCode to ${NEW_VERSION_NAME} and ${NEW_VERSION_CODE}" 124 | 125 | # Push changes to the current branch 126 | git push https://github-actions:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} HEAD:${GITHUB_REF#refs/heads/} 127 | 128 | - name: Rename APK 129 | run: | 130 | ls -al app/build/outputs/apk/release 131 | echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}" 132 | cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher_${{ env.NEW_VERSION_NAME }}.apk 133 | 134 | - name: Upload APK 135 | uses: actions/upload-artifact@v4.3.5 136 | with: 137 | name: KernelFlasher_${{ env.NEW_VERSION_NAME }} 138 | path: KernelFlasher_${{ env.NEW_VERSION_NAME }}.apk 139 | 140 | - name: Upload release 141 | uses: ncipollo/release-action@v1.14.0 142 | with: 143 | allowUpdates: true 144 | removeArtifacts: true 145 | draft: true 146 | name: ${{ env.NEW_VERSION_NAME }} 147 | tag: "v${{ env.NEW_VERSION_NAME }}" 148 | body: | 149 | Note: KernelFlasher + allow-errors 150 | 151 | Changes in this Release: 152 | ${{ github.event.inputs.changes_in_release }} 153 | artifacts: "*.apk" 154 | -------------------------------------------------------------------------------- /.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 | .secrets 13 | .env 14 | .github/workflows/build_local.yml 15 | KernelFlasher.apk 16 | /app/build/* -------------------------------------------------------------------------------- /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/fatalcoder524/KernelFlasher)](https://GitHub.com/fatalcoder524/KernelFlasher/releases/) 2 | [![Github all releases](https://img.shields.io/github/downloads/fatalcoder524/KernelFlasher/total)](https://GitHub.com/fatalcoder524/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 2 | /release 3 | build.gradle.bak 4 | *.bak -------------------------------------------------------------------------------- /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 | defaultConfig { 12 | applicationId "com.github.capntrips.kernelflasher" 13 | minSdk 29 14 | targetSdk 34 15 | versionCode 10500 16 | versionName "1.5.0" 17 | 18 | javaCompileOptions { 19 | annotationProcessorOptions { 20 | arguments += [ 21 | "room.schemaLocation": "$projectDir/schemas".toString(), 22 | "room.incremental": "true" 23 | ] 24 | } 25 | } 26 | ndk { 27 | //noinspection ChromeOsAbiSupport 28 | abiFilters = ['armeabi-v7a', 'arm64-v8a'] 29 | } 30 | vectorDrawables { 31 | useSupportLibrary true 32 | } 33 | } 34 | buildTypes { 35 | release { 36 | minifyEnabled false 37 | } 38 | } 39 | sourceSets { 40 | main { 41 | jniLibs.srcDirs = ['src/main/jniLibs'] 42 | } 43 | } 44 | buildFeatures { 45 | aidl true 46 | } 47 | compileOptions { 48 | sourceCompatibility JavaVersion.VERSION_17 49 | targetCompatibility JavaVersion.VERSION_17 50 | } 51 | kotlinOptions { 52 | jvmTarget = '17' 53 | } 54 | buildFeatures { 55 | compose true 56 | } 57 | composeOptions { 58 | kotlinCompilerExtensionVersion libs.versions.compose.compiler.get() 59 | } 60 | packagingOptions { 61 | resources { 62 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 63 | } 64 | jniLibs { 65 | useLegacyPackaging true 66 | } 67 | } 68 | namespace 'com.github.capntrips.kernelflasher' 69 | } 70 | 71 | dependencies { 72 | implementation(libs.androidx.activity.compose) 73 | implementation(libs.androidx.appcompat) 74 | implementation(libs.androidx.compose.material) 75 | implementation(libs.androidx.compose.material3) 76 | implementation(libs.androidx.compose.foundation) 77 | implementation(libs.androidx.compose.ui) 78 | implementation(libs.androidx.core.ktx) 79 | implementation(libs.androidx.core.splashscreen) 80 | implementation(libs.androidx.lifecycle.runtime.ktx) 81 | implementation(libs.androidx.lifecycle.viewmodel.compose) 82 | implementation(libs.androidx.navigation.compose) 83 | implementation(libs.androidx.room.runtime) 84 | annotationProcessor(libs.androidx.room.compiler) 85 | ksp(libs.androidx.room.compiler) 86 | implementation(libs.libsu.core) 87 | implementation(libs.libsu.io) 88 | implementation(libs.libsu.nio) 89 | implementation(libs.libsu.service) 90 | implementation(libs.material) 91 | implementation(libs.okhttp) 92 | implementation(libs.kotlinx.serialization.json) 93 | implementation 'com.squareup.retrofit2:retrofit:2.11.0' 94 | implementation 'com.squareup.retrofit2:converter-gson:2.11.0' 95 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /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/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/assets/ksuinit -------------------------------------------------------------------------------- /app/src/main/assets/mkbootfs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/assets/mkbootfs -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/AppUpdater.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.DownloadManager 5 | import android.content.BroadcastReceiver 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.content.IntentFilter 9 | import android.net.Uri 10 | import android.os.Environment 11 | import android.widget.Toast 12 | import com.google.gson.Gson 13 | import com.google.gson.annotations.SerializedName 14 | import retrofit2.Response 15 | import retrofit2.Retrofit 16 | import retrofit2.converter.gson.GsonConverterFactory 17 | import retrofit2.http.GET 18 | import java.io.IOException 19 | import java.net.HttpURLConnection 20 | import java.net.URL 21 | import kotlinx.coroutines.Dispatchers 22 | import kotlinx.coroutines.withContext 23 | 24 | interface GitHubApi { 25 | @GET("repos/fatalcoder524/KernelFlasher/releases/latest") 26 | suspend fun getLatestRelease(): Response 27 | } 28 | 29 | object AppUpdater { 30 | 31 | data class GitHubAsset( 32 | val name: String, 33 | @SerializedName("browser_download_url") val downloadUrl: String 34 | ) 35 | 36 | data class GitHubRelease( 37 | @SerializedName("tag_name") val tagName: String, 38 | val body: String, 39 | val assets: List 40 | ) 41 | 42 | private val api: GitHubApi = Retrofit.Builder() 43 | .baseUrl("https://api.github.com/") 44 | .addConverterFactory(GsonConverterFactory.create(Gson())) 45 | .build() 46 | .create(GitHubApi::class.java) 47 | 48 | // Compares version strings (e.g., v1.0.0 vs. v1.0.1) 49 | private fun isNewer(latest: String, current: String): Boolean { 50 | val latestParts = latest.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 } 51 | val currentParts = current.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 } 52 | 53 | return latestParts.zip(currentParts).any { (l, c) -> l > c } 54 | } 55 | 56 | suspend fun hasActiveInternetConnection(): Boolean = withContext(Dispatchers.IO) { 57 | try { 58 | val url = URL("https://connectivitycheck.gstatic.com/generate_204") 59 | val connection = url.openConnection() as HttpURLConnection 60 | connection.setRequestProperty("User-Agent", "Android") 61 | connection.connectTimeout = 1500 62 | connection.connect() 63 | return@withContext connection.responseCode == 204 64 | } catch (e: IOException) { 65 | return@withContext false 66 | } 67 | } 68 | 69 | // Checks if an update is available 70 | suspend fun checkForUpdate( 71 | context: Context, 72 | currentVersion: String, 73 | onShowDialog: (String, List, () -> Unit) -> Unit 74 | ) { 75 | val response = api.getLatestRelease() 76 | if (response.isSuccessful) { 77 | val release = response.body() ?: return 78 | val latestVersion = release.tagName.removePrefix("v") 79 | if (isNewer(latestVersion, currentVersion)) { 80 | val apk = release.assets.find { it.name.endsWith(".apk") } ?: return 81 | val dialogTitle = "New version: $latestVersion" 82 | val dialogLines = listOf( 83 | "Changelog:", 84 | *release.body.split("\n").toTypedArray() 85 | ) 86 | val confirmAction = { downloadAndInstallApk(context, apk.downloadUrl, latestVersion) } 87 | onShowDialog(dialogTitle, dialogLines, confirmAction) 88 | } 89 | } 90 | } 91 | 92 | @SuppressLint("UnspecifiedRegisterReceiverFlag") 93 | private fun downloadAndInstallApk(context: Context, url: String, latestVersion: String) { 94 | Toast.makeText(context, "Downloading Update in Background. Don't perform any operations till update is completed!", Toast.LENGTH_LONG).show() 95 | val request = DownloadManager.Request(Uri.parse(url)) 96 | request.setTitle("Kernel Flasher Latest Download") 97 | request.setDescription("Downloading update...") 98 | request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Kernel_Flasher_$latestVersion.apk") 99 | request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) 100 | 101 | val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager 102 | val id = manager.enqueue(request) 103 | 104 | val receiver = object : BroadcastReceiver() { 105 | override fun onReceive(c: Context?, intent: Intent?) { 106 | val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) 107 | if (id == downloadId) { 108 | val apkUri = manager.getUriForDownloadedFile(id) 109 | val installIntent = Intent(Intent.ACTION_VIEW).apply { 110 | setDataAndType(apkUri, "application/vnd.android.package-archive") 111 | flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION 112 | } 113 | context.startActivity(installIntent) 114 | } 115 | } 116 | } 117 | 118 | val appContext = context.applicationContext 119 | val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) 120 | 121 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { 122 | appContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED) 123 | } else { 124 | @Suppress("DEPRECATION") 125 | appContext.registerReceiver(receiver, intentFilter) 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /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 | override fun onBind(intent: Intent): IBinder { 15 | return FilesystemIPC() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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() && result[0].trim().startsWith("{")) { 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(context: Context, partitionName: String, slotSuffix: String): ExtendedFile? { 61 | var blockDevice: ExtendedFile? = null 62 | val fstabEntry = findPartitionFstabEntry(context, partitionName) 63 | if (fstabEntry != null) { 64 | if (fstabEntry.fsMgrFlags?.logical == true) { 65 | if (fstabEntry.logicalPartitionName == "$partitionName$slotSuffix") { 66 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) 67 | } 68 | } else { 69 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) 70 | if (blockDevice.name != "$partitionName$slotSuffix") { 71 | blockDevice = fileSystemManager!!.getFile(blockDevice.parentFile, "$partitionName$slotSuffix") 72 | } 73 | } 74 | } 75 | if (blockDevice == null || !blockDevice.exists()) { 76 | val siblingDevice = if (bootParent != null) fileSystemManager!!.getFile(bootParent!!, "$partitionName$slotSuffix") else null 77 | val physicalDevice = fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix") 78 | val logicalDevice = fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix") 79 | if (siblingDevice?.exists() == true) { 80 | blockDevice = physicalDevice 81 | } else if (physicalDevice.exists()) { 82 | blockDevice = physicalDevice 83 | } else if (logicalDevice.exists()) { 84 | blockDevice = logicalDevice 85 | } 86 | } 87 | return blockDevice 88 | } 89 | 90 | @Suppress("unused") 91 | fun partitionAvb(context: Context, partitionName: String): String { 92 | val httools = File(context.filesDir, "httools_static") 93 | val result = Shell.cmd("$httools avb $partitionName").exec().out 94 | return if (result.isNotEmpty()) result[0] else "" 95 | } 96 | 97 | fun flashBlockDevice(image: ExtendedFile, blockDevice: ExtendedFile, hashAlgorithm: String): String { 98 | val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt() 99 | val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() 100 | if (partitionSize < imageSize) { 101 | throw Error("Partition ${blockDevice.name} is smaller than image") 102 | } 103 | if (partitionSize > imageSize) { 104 | Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice").exec() 105 | } 106 | val messageDigest = MessageDigest.getInstance(hashAlgorithm) 107 | image.newInputStream().use { inputStream -> 108 | blockDevice.newOutputStream().use { outputStream -> 109 | DigestOutputStream(outputStream, messageDigest).use { digestOutputStream -> 110 | inputStream.copyTo(digestOutputStream) 111 | } 112 | } 113 | } 114 | return messageDigest.digest().toHex() 115 | } 116 | 117 | @Suppress("SameParameterValue") 118 | fun flashLogicalPartition(context: Context, image: ExtendedFile, blockDevice: ExtendedFile, partitionName: String, slotSuffix: String, hashAlgorithm: String, addMessage: (message: String) -> Unit): String { 119 | val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() 120 | val lptools = File(context.filesDir, "lptools_static") 121 | Shell.cmd("$lptools remove ${partitionName}_kf").exec() 122 | if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) { 123 | if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) { 124 | if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) { 125 | val temporaryBlockDevice = fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf") 126 | val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm) 127 | if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix").exec().isSuccess) { 128 | return hash 129 | } else { 130 | throw Error("Replacing $partitionName$slotSuffix failed") 131 | } 132 | } else { 133 | throw Error("Remapping ${partitionName}_kf failed") 134 | } 135 | } else { 136 | throw Error("Unmapping ${partitionName}_kf failed") 137 | } 138 | } else { 139 | addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...") 140 | val httools = File(context.filesDir, "httools_static") 141 | if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) { 142 | val verityBlockDevice = blockDevice.parentFile!!.getChildFile("${partitionName}-verity") 143 | if (verityBlockDevice.exists()) { 144 | if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) { 145 | throw Error("Unmapping ${partitionName}-verity failed") 146 | } 147 | } 148 | if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) { 149 | if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)").exec().isSuccess) { 150 | if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) { 151 | val hash = flashBlockDevice(image, blockDevice, hashAlgorithm) 152 | if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) { 153 | return hash 154 | } else { 155 | throw Error("Mounting $partitionName failed") 156 | } 157 | } else { 158 | throw Error("Remapping $partitionName$slotSuffix failed") 159 | } 160 | } else { 161 | throw Error("Resizing $partitionName$slotSuffix failed") 162 | } 163 | } else { 164 | throw Error("Unmapping $partitionName$slotSuffix failed") 165 | } 166 | } else { 167 | throw Error("Unmounting $partitionName failed") 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /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 = inputStream().reader(charset) 11 | 12 | private fun ExtendedFile.writeBytes(array: kotlin.ByteArray): Unit = outputStream().use { it.write(array) } 13 | 14 | fun ExtendedFile.readText(charset: Charset = Charsets.UTF_8): String = reader(charset).use { it.readText() } 15 | 16 | @Suppress("unused") 17 | fun ExtendedFile.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset)) 18 | 19 | fun ExtendedFile.inputStream(): InputStream = newInputStream() 20 | 21 | fun ExtendedFile.outputStream(): OutputStream = newOutputStream() 22 | } 23 | -------------------------------------------------------------------------------- /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(boot, dtbo, init_boot, recovery, system_dlkm, vbmeta, vendor_boot, vendor_dlkm, vendor_kernel_boot) 30 | }.partitions 31 | } 32 | 33 | fun get(partition: String): String? { 34 | return when (partition) { 35 | "boot" -> boot 36 | "dtbo" -> dtbo 37 | "init_boot" -> init_boot 38 | "recovery" -> recovery 39 | "system_dlkm" -> system_dlkm 40 | "vbmeta" -> vbmeta 41 | "vendor_boot" -> vendor_boot 42 | "vendor_dlkm" -> vendor_dlkm 43 | "vendor_kernel_boot" -> vendor_kernel_boot 44 | else -> null 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /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) = encoder.encodeString(formatter.format(value)) 25 | override fun deserialize(decoder: Decoder): Date = formatter.parse(decoder.decodeString())!! 26 | } 27 | 28 | object UpdateSerializer : JsonTransformingSerializer(Update.serializer()) { 29 | override fun transformSerialize(element: JsonElement): JsonElement { 30 | require(element is JsonObject) 31 | return buildJsonObject { 32 | put("kernel", buildJsonObject { 33 | put("name", element["kernelName"]!!) 34 | put("version", element["kernelVersion"]!!) 35 | put("link", element["kernelLink"]!!) 36 | put("changelog_url", element["kernelChangelogUrl"]!!) 37 | put("date", element["kernelDate"]!!) 38 | put("sha1", element["kernelSha1"]!!) 39 | }) 40 | if (element["supportLink"] != null) { 41 | put("support", buildJsonObject { 42 | put("link", element["supportLink"]!!) 43 | }) 44 | } 45 | } 46 | } 47 | override fun transformDeserialize(element: JsonElement): JsonElement { 48 | require(element is JsonObject) 49 | val kernel = element["kernel"] 50 | val support = element["support"] 51 | require(kernel is JsonObject) 52 | require(support is JsonObject?) 53 | return buildJsonObject { 54 | put("kernelName", kernel["name"]!!) 55 | put("kernelVersion", kernel["version"]!!) 56 | put("kernelLink", kernel["link"]!!) 57 | put("kernelChangelogUrl", kernel["changelog_url"]!!) 58 | put("kernelDate", kernel["date"]!!) 59 | put("kernelSha1", kernel["sha1"]!!) 60 | if (support != null && support["link"] != null) { 61 | put("supportLink", support["link"]!!) 62 | } 63 | } 64 | } 65 | } 66 | 67 | @Entity 68 | @Serializable 69 | data class Update( 70 | @PrimaryKey 71 | @Transient 72 | val id: Int? = null, 73 | @ColumnInfo(name = "update_uri") 74 | @Transient 75 | var updateUri: String? = null, 76 | @ColumnInfo(name = "kernel_name") 77 | var kernelName: String, 78 | @ColumnInfo(name = "kernel_version") 79 | var kernelVersion: String, 80 | @ColumnInfo(name = "kernel_link") 81 | var kernelLink: String, 82 | @ColumnInfo(name = "kernel_changelog_url") 83 | var kernelChangelogUrl: String, 84 | @ColumnInfo(name = "kernel_date") 85 | @Serializable(DateSerializer::class) 86 | var kernelDate: Date, 87 | @ColumnInfo(name = "kernel_sha1") 88 | var kernelSha1: String, 89 | @ColumnInfo(name = "support_link") 90 | var supportLink: String?, 91 | @ColumnInfo(name = "last_updated") 92 | @Transient 93 | var lastUpdated: Date? = null, 94 | ) 95 | -------------------------------------------------------------------------------- /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/Card.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Surface 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.unit.Dp 16 | import androidx.compose.ui.unit.dp 17 | 18 | // TODO: Remove when card is supported in material3: https://m3.material.io/components/cards/implementation/android 19 | @Composable 20 | fun Card( 21 | shape: Shape = RoundedCornerShape(4.dp), 22 | backgroundColor: Color = MaterialTheme.colorScheme.surface, 23 | contentColor: Color = MaterialTheme.colorScheme.onSurface, 24 | border: BorderStroke? = null, 25 | tonalElevation: Dp = 2.dp, 26 | shadowElevation: Dp = 1.dp, 27 | content: @Composable ColumnScope.() -> Unit 28 | ) { 29 | Surface( 30 | shape = shape, 31 | color = backgroundColor, 32 | contentColor = contentColor, 33 | tonalElevation = tonalElevation, 34 | shadowElevation = shadowElevation, 35 | border = border 36 | ) { 37 | Column( 38 | modifier = Modifier 39 | .fillMaxWidth() 40 | .padding(18.dp, (13.788).dp, 18.dp, 18.dp), 41 | content = content 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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.ColumnScope 5 | import androidx.compose.foundation.layout.Row 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.foundation.layout.padding 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun DataCard( 19 | title: String, 20 | button: @Composable (() -> Unit)? = null, 21 | content: @Composable (ColumnScope.() -> Unit)? = null 22 | ) { 23 | Card { 24 | Row( 25 | modifier = Modifier 26 | .fillMaxWidth() 27 | .padding(0.dp), 28 | horizontalArrangement = Arrangement.SpaceBetween, 29 | verticalAlignment = Alignment.CenterVertically 30 | ) { 31 | Text( 32 | modifier = Modifier.padding(0.dp, 9.dp, 8.dp, 9.dp).weight(1.0f), 33 | text = title, 34 | color = MaterialTheme.colorScheme.primary, 35 | style = MaterialTheme.typography.titleLarge 36 | ) 37 | if (button != null) { 38 | button() 39 | } 40 | } 41 | if (content != null) { 42 | Spacer(Modifier.height(10.dp)) 43 | content() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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.width 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.MutableState 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.layout.layout 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun DataRow( 20 | label: String, 21 | value: String, 22 | labelColor: Color = Color.Unspecified, 23 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium, 24 | valueColor: Color = Color.Unspecified, 25 | valueStyle: TextStyle = MaterialTheme.typography.titleSmall, 26 | mutableMaxWidth: MutableState? = null, 27 | clickable: Boolean = false, 28 | ) { 29 | Row { 30 | val modifier = if (mutableMaxWidth != null) { 31 | var maxWidth by mutableMaxWidth 32 | Modifier 33 | .layout { measurable, constraints -> 34 | val placeable = measurable.measure(constraints) 35 | maxWidth = maxOf(maxWidth, placeable.width) 36 | layout(width = maxWidth, height = placeable.height) { 37 | placeable.placeRelative(0, 0) 38 | } 39 | } 40 | .alignByBaseline() 41 | } else { 42 | Modifier 43 | .alignByBaseline() 44 | } 45 | Text( 46 | modifier = modifier, 47 | text = label, 48 | color = labelColor, 49 | style = labelStyle 50 | ) 51 | Spacer(Modifier.width(8.dp)) 52 | DataValue(value, valueColor, valueStyle, clickable) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /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.RowScope 5 | import androidx.compose.foundation.text.selection.SelectionContainer 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.runtime.setValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.text.style.TextOverflow 17 | 18 | @Composable 19 | fun RowScope.DataValue( 20 | value: String, 21 | color: Color = Color.Unspecified, 22 | style: TextStyle = MaterialTheme.typography.titleSmall, 23 | clickable: Boolean = false, 24 | ) { 25 | SelectionContainer(Modifier.alignByBaseline()) { 26 | var clicked by remember { mutableStateOf(false) } 27 | val modifier = if (clickable) { 28 | Modifier 29 | .clickable { clicked = !clicked } 30 | .alignByBaseline() 31 | } else { 32 | Modifier 33 | .alignByBaseline() 34 | } 35 | Text( 36 | modifier = modifier, 37 | text = value, 38 | color = color, 39 | style = style, 40 | maxLines = if (clicked) Int.MAX_VALUE else 1, 41 | overflow = if (clicked) TextOverflow.Visible else TextOverflow.Ellipsis 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.material3.MaterialTheme 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(buttonText, 30 | maxLines = 1, 31 | color = MaterialTheme.colorScheme.primary 32 | ) 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.rememberLauncherForActivityResult 7 | import androidx.activity.result.contract.ActivityResultContracts 8 | import androidx.compose.animation.ExperimentalAnimationApi 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.ExperimentalMaterialApi 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.OutlinedButton 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.platform.LocalContext 20 | import androidx.compose.ui.unit.ExperimentalUnitApi 21 | import androidx.compose.ui.unit.dp 22 | import com.github.capntrips.kernelflasher.MainActivity 23 | import kotlinx.serialization.ExperimentalSerializationApi 24 | 25 | @ExperimentalAnimationApi 26 | @ExperimentalMaterialApi 27 | @ExperimentalMaterial3Api 28 | @ExperimentalSerializationApi 29 | @ExperimentalUnitApi 30 | @Composable 31 | fun FlashButton( 32 | buttonText: String, 33 | validExtension: String, 34 | callback: (uri: Uri) -> Unit 35 | ) { 36 | val mainActivity = LocalContext.current as MainActivity 37 | val result = remember { mutableStateOf(null) } 38 | val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { 39 | result.value = it 40 | if (it == null) { 41 | mainActivity.isAwaitingResult = false 42 | } 43 | } 44 | OutlinedButton( 45 | modifier = Modifier 46 | .fillMaxWidth(), 47 | shape = RoundedCornerShape(4.dp), 48 | onClick = { 49 | mainActivity.isAwaitingResult = true 50 | launcher.launch("*/*") 51 | } 52 | ) { 53 | Text(buttonText) 54 | } 55 | result.value?.let {uri -> 56 | if (mainActivity.isAwaitingResult) { 57 | val contentResolver = mainActivity.contentResolver 58 | val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor -> 59 | val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) 60 | if (nameIndex != -1 && cursor.moveToFirst()) { 61 | cursor.getString(nameIndex) 62 | } else { 63 | null 64 | } 65 | } 66 | 67 | if (fileName != null && fileName.endsWith(validExtension, ignoreCase = true)) { 68 | callback.invoke(uri) 69 | } 70 | else { 71 | // Invalid file extension, show an error message or handle it 72 | Toast.makeText(mainActivity.applicationContext, "Invalid file selected!", Toast.LENGTH_LONG).show() 73 | } 74 | } 75 | mainActivity.isAwaitingResult = false 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /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(message, 72 | style = LocalTextStyle.current.copy( 73 | fontFamily = FontFamily.Monospace, 74 | fontSize = TextUnit(12.0f, TextUnitType.Sp), 75 | lineHeight = TextUnit(18.0f, TextUnitType.Sp) 76 | ) 77 | ) 78 | } 79 | } 80 | content() 81 | } 82 | 83 | // https://stackoverflow.com/a/68056586/434343 84 | fun Modifier.scrollbar( 85 | state: LazyListState, 86 | width: Dp = 6.dp 87 | ): Modifier = composed { 88 | var visibleItemsCountChanged = false 89 | var visibleItemsCount by remember { mutableIntStateOf(state.layoutInfo.visibleItemsInfo.size) } 90 | if (visibleItemsCount != state.layoutInfo.visibleItemsInfo.size) { 91 | visibleItemsCountChanged = true 92 | visibleItemsCount = state.layoutInfo.visibleItemsInfo.size 93 | } 94 | 95 | val hidden = state.layoutInfo.visibleItemsInfo.size == state.layoutInfo.totalItemsCount 96 | val targetAlpha = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0.5f else 0f 97 | val delay = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0 else 250 98 | val duration = if (hidden || visibleItemsCountChanged) 0 else if (state.isScrollInProgress) 150 else 500 99 | 100 | val alpha by animateFloatAsState( 101 | targetValue = targetAlpha, 102 | animationSpec = tween(delayMillis = delay, durationMillis = duration) 103 | ) 104 | 105 | drawWithContent { 106 | drawContent() 107 | 108 | val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index 109 | val needDrawScrollbar = state.isScrollInProgress || visibleItemsCountChanged || alpha > 0.0f 110 | 111 | if (needDrawScrollbar && firstVisibleElementIndex != null) { 112 | val elementHeight = this.size.height / state.layoutInfo.totalItemsCount 113 | val scrollbarOffsetY = firstVisibleElementIndex * elementHeight 114 | val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight 115 | 116 | drawRoundRect( 117 | color = Color.Gray, 118 | topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), 119 | size = Size(width.toPx(), scrollbarHeight), 120 | cornerRadius = CornerRadius(width.toPx(), width.toPx()), 121 | alpha = alpha 122 | ) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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("%s, %s", stringResource(R.string.exists), stringResource(R.string.unmounted)) 65 | } 66 | } 67 | DataRow(stringResource(R.string.vendor_dlkm), vendorDlkmValue, mutableMaxWidth = cardWidth) 68 | } 69 | DataRow( 70 | label = stringResource(R.string.boot_fmt), 71 | value = viewModel.bootInfo.bootFmt ?: stringResource(R.string.not_found), 72 | mutableMaxWidth = cardWidth 73 | ) 74 | DataRow( 75 | label = if (viewModel.bootInfo.ramdiskLocation == "init_boot.img") stringResource(R.string.init_boot_fmt) else stringResource(R.string.ramdisk_fmt), 76 | value = viewModel.bootInfo.initBootFmt ?: stringResource(R.string.not_found), 77 | mutableMaxWidth = cardWidth 78 | ) 79 | if (!viewModel.isRefreshing.value && viewModel.hasError) { 80 | Row { 81 | DataValue( 82 | value = viewModel.error ?: "", 83 | color = MaterialTheme.colorScheme.error, 84 | style = MaterialTheme.typography.titleSmall, 85 | clickable = true 86 | ) 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /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.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.ColumnScope 9 | import androidx.compose.foundation.layout.WindowInsets 10 | import androidx.compose.foundation.layout.WindowInsetsSides 11 | import androidx.compose.foundation.layout.asPaddingValues 12 | import androidx.compose.foundation.layout.fillMaxSize 13 | import androidx.compose.foundation.layout.fillMaxWidth 14 | import androidx.compose.foundation.layout.navigationBars 15 | import androidx.compose.foundation.layout.only 16 | import androidx.compose.foundation.layout.padding 17 | import androidx.compose.foundation.layout.statusBars 18 | import androidx.compose.foundation.rememberScrollState 19 | import androidx.compose.foundation.verticalScroll 20 | import androidx.compose.material.ExperimentalMaterialApi 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.filled.ArrowBack 23 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 24 | import androidx.compose.material.pullrefresh.pullRefresh 25 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 26 | import androidx.compose.material3.ExperimentalMaterial3Api 27 | import androidx.compose.material3.Icon 28 | import androidx.compose.material3.IconButton 29 | import androidx.compose.material3.MaterialTheme 30 | import androidx.compose.material3.Scaffold 31 | import androidx.compose.material3.Text 32 | import androidx.compose.runtime.Composable 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.platform.LocalContext 36 | import androidx.compose.ui.res.stringResource 37 | import androidx.compose.ui.unit.dp 38 | import androidx.navigation.NavController 39 | import com.github.capntrips.kernelflasher.R 40 | import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel 41 | import kotlinx.serialization.ExperimentalSerializationApi 42 | 43 | @ExperimentalMaterialApi 44 | @ExperimentalMaterial3Api 45 | @ExperimentalSerializationApi 46 | @Composable 47 | fun RefreshableScreen( 48 | viewModel: MainViewModel, 49 | navController: NavController, 50 | swipeEnabled: Boolean = false, 51 | content: @Composable ColumnScope.() -> Unit 52 | ) { 53 | val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues() 54 | val navigationBars = WindowInsets.navigationBars.asPaddingValues() 55 | val context = LocalContext.current 56 | val state = rememberPullRefreshState(viewModel.isRefreshing, onRefresh = { 57 | viewModel.refresh(context) 58 | }) 59 | Scaffold( 60 | topBar = { 61 | Box( 62 | Modifier 63 | .fillMaxWidth() 64 | .padding(statusBar)) { 65 | if (navController.previousBackStackEntry != null) { 66 | AnimatedVisibility( 67 | !viewModel.isRefreshing, 68 | enter = fadeIn(), 69 | exit = fadeOut() 70 | ) { 71 | IconButton( 72 | onClick = { navController.popBackStack() }, 73 | modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp) 74 | ) { 75 | Icon( 76 | Icons.Filled.ArrowBack, 77 | contentDescription = stringResource(R.string.back), 78 | tint = MaterialTheme.colorScheme.onSurface 79 | ) 80 | } 81 | } 82 | } 83 | Box( 84 | Modifier 85 | .fillMaxWidth() 86 | .padding(16.dp)) { 87 | Text( 88 | modifier = Modifier.align(Alignment.Center), 89 | text = stringResource(R.string.app_name), 90 | style = MaterialTheme.typography.headlineSmall 91 | ) 92 | } 93 | } 94 | } 95 | ) { paddingValues -> 96 | Box( 97 | modifier = Modifier 98 | .padding(paddingValues) 99 | .pullRefresh(state, swipeEnabled) 100 | .fillMaxSize(), 101 | ) { 102 | Column( 103 | modifier = Modifier 104 | .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding()) 105 | .fillMaxSize() 106 | .verticalScroll(rememberScrollState()), 107 | content = content 108 | ) 109 | PullRefreshIndicator( 110 | viewModel.isRefreshing, 111 | state = state, 112 | modifier = Modifier.align(Alignment.TopCenter), 113 | backgroundColor = MaterialTheme.colorScheme.background, 114 | contentColor = MaterialTheme.colorScheme.primaryContainer, 115 | scale = true 116 | ) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /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.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.font.FontFamily 21 | import androidx.compose.ui.text.font.FontStyle 22 | import androidx.compose.ui.text.font.FontWeight 23 | import androidx.compose.ui.text.style.TextAlign 24 | import androidx.compose.ui.unit.dp 25 | import androidx.navigation.NavController 26 | import com.github.capntrips.kernelflasher.R 27 | import com.github.capntrips.kernelflasher.common.PartitionUtil 28 | import com.github.capntrips.kernelflasher.ui.components.DataCard 29 | import com.github.capntrips.kernelflasher.ui.components.DataRow 30 | import com.github.capntrips.kernelflasher.ui.components.DataSet 31 | import com.github.capntrips.kernelflasher.ui.components.ViewButton 32 | 33 | @ExperimentalMaterial3Api 34 | @Composable 35 | fun ColumnScope.BackupsContent( 36 | viewModel: BackupsViewModel, 37 | navController: NavController 38 | ) { 39 | val context = LocalContext.current 40 | if (viewModel.currentBackup != null && viewModel.backups.containsKey(viewModel.currentBackup)) { 41 | DataCard (viewModel.currentBackup!!) { 42 | val cardWidth = remember { mutableIntStateOf(0) } 43 | val currentBackup = viewModel.backups.getValue(viewModel.currentBackup!!) 44 | DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth) 45 | DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 46 | if (currentBackup.type == "raw") { 47 | if (!currentBackup.bootSha1.isNullOrEmpty()) { 48 | DataRow( 49 | label = stringResource(R.string.boot_sha1), 50 | value = currentBackup.bootSha1.substring(0, 8), 51 | valueStyle = MaterialTheme.typography.titleSmall.copy( 52 | fontFamily = FontFamily.Monospace, 53 | fontWeight = FontWeight.Medium 54 | ), 55 | mutableMaxWidth = cardWidth 56 | ) 57 | } 58 | if (currentBackup.hashes != null) { 59 | val hashWidth = remember { mutableIntStateOf(0) } 60 | DataSet(stringResource(R.string.hashes)) { 61 | for (partitionName in PartitionUtil.PartitionNames) { 62 | val hash = currentBackup.hashes.get(partitionName) 63 | if (hash != null) { 64 | DataRow( 65 | label = partitionName, 66 | value = hash.takeIf { it.isNotEmpty() }?.substring(0, 8) ?: "Hash not found!", 67 | valueStyle = MaterialTheme.typography.titleSmall.copy( 68 | fontFamily = FontFamily.Monospace, 69 | fontWeight = FontWeight.Medium 70 | ), 71 | mutableMaxWidth = hashWidth 72 | ) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | AnimatedVisibility(!viewModel.isRefreshing) { 80 | Column { 81 | Spacer(Modifier.height(5.dp)) 82 | OutlinedButton( 83 | modifier = Modifier 84 | .fillMaxWidth(), 85 | shape = RoundedCornerShape(4.dp), 86 | onClick = { viewModel.delete(context) { navController.popBackStack() } } 87 | ) { 88 | Text(stringResource(R.string.delete)) 89 | } 90 | } 91 | } 92 | } else { 93 | DataCard(stringResource(R.string.backups)) 94 | AnimatedVisibility(viewModel.needsMigration) { 95 | Column { 96 | Spacer(Modifier.height(5.dp)) 97 | OutlinedButton( 98 | modifier = Modifier 99 | .fillMaxWidth(), 100 | shape = RoundedCornerShape(4.dp), 101 | onClick = { viewModel.migrate(context) } 102 | ) { 103 | Text(stringResource(R.string.migrate)) 104 | } 105 | } 106 | } 107 | if (viewModel.backups.isNotEmpty()) { 108 | for (id in viewModel.backups.keys.sortedByDescending { it }) { 109 | val currentBackup = viewModel.backups[id]!! 110 | Spacer(Modifier.height(16.dp)) 111 | DataCard( 112 | title = id, 113 | button = { 114 | AnimatedVisibility(!viewModel.isRefreshing) { 115 | Column { 116 | ViewButton(onClick = { 117 | navController.navigate("backups/$id") 118 | }) 119 | } 120 | } 121 | } 122 | ) { 123 | val cardWidth = remember { mutableIntStateOf(0) } 124 | if (currentBackup.type == "raw" && !currentBackup.bootSha1.isNullOrEmpty()) { 125 | DataRow( 126 | label = stringResource(R.string.boot_sha1), 127 | value = currentBackup.bootSha1.substring(0, 8), 128 | valueStyle = MaterialTheme.typography.titleSmall.copy( 129 | fontFamily = FontFamily.Monospace, 130 | fontWeight = FontWeight.Medium 131 | ), 132 | mutableMaxWidth = cardWidth 133 | ) 134 | } 135 | DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 136 | } 137 | } 138 | } else { 139 | Spacer(Modifier.height(32.dp)) 140 | Text( 141 | stringResource(R.string.no_backups_found), 142 | modifier = Modifier.fillMaxWidth(), 143 | textAlign = TextAlign.Center, 144 | fontStyle = FontStyle.Italic 145 | ) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /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 kotlin.DeprecationLevel 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.launch 27 | import kotlinx.coroutines.withContext 28 | import kotlinx.serialization.json.Json 29 | import java.io.File 30 | import java.io.FileInputStream 31 | import java.time.LocalDateTime 32 | import java.time.format.DateTimeFormatter 33 | import java.util.Properties 34 | 35 | class BackupsViewModel( 36 | context: Context, 37 | private val fileSystemManager: FileSystemManager, 38 | private val navController: NavController, 39 | private val _isRefreshing: MutableState, 40 | private val _backups: MutableMap 41 | ) : ViewModel() { 42 | companion object { 43 | const val TAG: String = "KernelFlasher/BackupsState" 44 | } 45 | 46 | private val _restoreOutput: SnapshotStateList = mutableStateListOf() 47 | var currentBackup: String? = null 48 | set(value) { 49 | if (value != field) { 50 | if (_backups[value]?.hashes != null) { 51 | PartitionUtil.AvailablePartitions.forEach { partitionName -> 52 | if (_backups[value]!!.hashes!!.get(partitionName) != null) { 53 | _backupPartitions[partitionName] = true 54 | } 55 | } 56 | } 57 | field = value 58 | } 59 | } 60 | var wasRestored: Boolean? = null 61 | private val _backupPartitions: SnapshotStateMap = mutableStateMapOf() 62 | private val hashAlgorithm: String = "SHA-256" 63 | @Deprecated("Backup migration will be removed in the first stable release", level = DeprecationLevel.WARNING) 64 | private var _needsMigration: MutableState = mutableStateOf(false) 65 | 66 | val restoreOutput: List 67 | get() = _restoreOutput 68 | val backupPartitions: MutableMap 69 | get() = _backupPartitions 70 | val isRefreshing: Boolean 71 | get() = _isRefreshing.value 72 | val backups: Map 73 | get() = _backups 74 | @Deprecated("Backup migration will be removed in the first stable release") 75 | val needsMigration: Boolean 76 | get() = _needsMigration.value 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(context: Context, source: ExtendedFile, slotSuffix: String): Partitions? { 173 | val partitions = HashMap() 174 | for (partitionName in PartitionUtil.PartitionNames) { 175 | if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) { 176 | val image = source.getChildFile("$partitionName.img") 177 | if (image.exists()) { 178 | val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) 179 | if (blockDevice != null && blockDevice.exists()) { 180 | addMessage("Restoring $partitionName") 181 | partitions[partitionName] = if (PartitionUtil.isPartitionLogical(context, partitionName)) { 182 | PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message -> 183 | addMessage(message) 184 | } 185 | } else { 186 | PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) 187 | } 188 | } else { 189 | log(context, "Partition $partitionName was not found", shouldThrow = true) 190 | } 191 | } 192 | } 193 | } 194 | if (partitions.isNotEmpty()) { 195 | return Partitions.from(partitions) 196 | } 197 | return null 198 | } 199 | 200 | fun restore(context: Context, slotSuffix: String) { 201 | launch { 202 | _clearRestore() 203 | @SuppressLint("SdCardPath") 204 | val externalDir = File("/sdcard/KernelFlasher") 205 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 206 | val backupDir = backupsDir.getChildFile(currentBackup!!) 207 | if (!backupDir.exists()) { 208 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 209 | return@launch 210 | } 211 | addMessage("Restoring backup $currentBackup") 212 | val hashes = restorePartitions(context, backupDir, slotSuffix) 213 | if (hashes == null) { 214 | log(context, "No partitions restored", shouldThrow = true) 215 | } 216 | addMessage("Backup $currentBackup restored") 217 | wasRestored = true 218 | } 219 | } 220 | 221 | fun delete(context: Context, callback: () -> Unit) { 222 | launch { 223 | @SuppressLint("SdCardPath") 224 | val externalDir = File("/sdcard/KernelFlasher") 225 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 226 | val backupDir = backupsDir.getChildFile(currentBackup!!) 227 | if (!backupDir.exists()) { 228 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 229 | return@launch 230 | } 231 | backupDir.deleteRecursively() 232 | _backups.remove(currentBackup!!) 233 | withContext(Dispatchers.Main) { 234 | callback.invoke() 235 | } 236 | } 237 | } 238 | 239 | @SuppressLint("SdCardPath") 240 | @Deprecated("Backup migration will be removed in the first stable release") 241 | fun migrate(context: Context) { 242 | launch { 243 | val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher") 244 | if (!externalDir.exists()) { 245 | if (!externalDir.mkdir()) { 246 | log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true) 247 | } 248 | } 249 | val backupsDir = externalDir.getChildFile("backups") 250 | if (!backupsDir.exists()) { 251 | if (!backupsDir.mkdir()) { 252 | log(context, "Failed to create backups dir", shouldThrow = true) 253 | } 254 | } 255 | val oldDir = context.getExternalFilesDir(null) 256 | val oldBackupsDir = File(oldDir, "backups") 257 | if (oldBackupsDir.exists()) { 258 | val indentedJson = Json { prettyPrint = true } 259 | val children = oldBackupsDir.listFiles() 260 | if (children != null) { 261 | for (child in children.sortedByDescending{it.name}) { 262 | if (!child.isDirectory) { 263 | child.delete() 264 | continue 265 | } 266 | val propFile = File(child, "backup.prop") 267 | @Suppress("BlockingMethodInNonBlockingContext") 268 | val inputStream = FileInputStream(propFile) 269 | val props = Properties() 270 | @Suppress("BlockingMethodInNonBlockingContext") 271 | props.load(inputStream) 272 | 273 | val name = child.name 274 | val type = props.getProperty("type", "raw") 275 | val kernelVersion = props.getProperty("kernel") 276 | val bootSha1 = if (type == "raw") props.getProperty("sha1") else null 277 | val filename = if (type == "ak3") "ak3.zip" else null 278 | propFile.delete() 279 | 280 | val dest = backupsDir.getChildFile(child.name) 281 | Shell.cmd("mv $child $dest").exec() 282 | if (!dest.exists()) { 283 | throw Error("Too slow") 284 | } 285 | val jsonFile = dest.getChildFile("backup.json") 286 | val backup = Backup(name, type, kernelVersion, bootSha1, filename) 287 | jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } 288 | _backups[name] = backup 289 | } 290 | } 291 | oldBackupsDir.delete() 292 | } 293 | refresh(context) 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /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.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.OutlinedButton 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.mutableIntStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.platform.LocalContext 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.unit.dp 20 | import androidx.navigation.NavController 21 | import com.github.capntrips.kernelflasher.R 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.SlotCard 25 | import kotlinx.serialization.ExperimentalSerializationApi 26 | 27 | @ExperimentalMaterial3Api 28 | @ExperimentalSerializationApi 29 | @Composable 30 | fun ColumnScope.MainContent( 31 | viewModel: MainViewModel, 32 | navController: NavController 33 | ) { 34 | val context = LocalContext.current 35 | DataCard (title = stringResource(R.string.device)) { 36 | val cardWidth = remember { mutableIntStateOf(0) } 37 | DataRow(stringResource(R.string.model), "${Build.MODEL} (${Build.DEVICE})", mutableMaxWidth = cardWidth) 38 | DataRow(stringResource(R.string.build_number), Build.ID, mutableMaxWidth = cardWidth) 39 | DataRow(stringResource(R.string.kernel_version), viewModel.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 40 | if (viewModel.isAb) { 41 | DataRow(stringResource(R.string.slot_suffix), viewModel.slotSuffix, mutableMaxWidth = cardWidth) 42 | } 43 | if(viewModel.susfsVersion != "v0.0.0" && viewModel.susfsVersion != "Invalid") 44 | { 45 | DataRow(stringResource(R.string.susfs_version), viewModel.susfsVersion, mutableMaxWidth = cardWidth) 46 | } 47 | } 48 | Spacer(Modifier.height(16.dp)) 49 | SlotCard( 50 | title = stringResource(if (viewModel.isAb) R.string.slot_a else R.string.slot), 51 | viewModel = viewModel.slotA, 52 | navController = navController 53 | ) 54 | if (viewModel.isAb) { 55 | Spacer(Modifier.height(16.dp)) 56 | SlotCard( 57 | title = stringResource(R.string.slot_b), 58 | viewModel = viewModel.slotB!!, 59 | navController = navController 60 | ) 61 | } 62 | Spacer(Modifier.height(16.dp)) 63 | AnimatedVisibility(!viewModel.isRefreshing) { 64 | OutlinedButton( 65 | modifier = Modifier 66 | .fillMaxWidth(), 67 | shape = RoundedCornerShape(4.dp), 68 | onClick = { navController.navigate("backups") } 69 | ) { 70 | Text(stringResource(R.string.backups)) 71 | } 72 | } 73 | AnimatedVisibility(!viewModel.isRefreshing) { 74 | OutlinedButton( 75 | modifier = Modifier 76 | .fillMaxWidth(), 77 | shape = RoundedCornerShape(4.dp), 78 | onClick = { navController.navigate("updates") } 79 | ) { 80 | Text(stringResource(R.string.updates)) 81 | } 82 | } 83 | if (viewModel.hasRamoops) { 84 | OutlinedButton( 85 | modifier = Modifier 86 | .fillMaxWidth(), 87 | shape = RoundedCornerShape(4.dp), 88 | onClick = { viewModel.saveRamoops(context) } 89 | ) { 90 | Text(stringResource(R.string.save_ramoops)) 91 | } 92 | } 93 | OutlinedButton( 94 | modifier = Modifier 95 | .fillMaxWidth(), 96 | shape = RoundedCornerShape(4.dp), 97 | onClick = { viewModel.saveDmesg(context) } 98 | ) { 99 | Text(stringResource(R.string.save_dmesg)) 100 | } 101 | OutlinedButton( 102 | modifier = Modifier 103 | .fillMaxWidth(), 104 | shape = RoundedCornerShape(4.dp), 105 | onClick = { viewModel.saveLogcat(context) } 106 | ) { 107 | Text(stringResource(R.string.save_logcat)) 108 | } 109 | AnimatedVisibility(!viewModel.isRefreshing) { 110 | OutlinedButton( 111 | modifier = Modifier 112 | .fillMaxWidth(), 113 | shape = RoundedCornerShape(4.dp), 114 | onClick = { navController.navigate("reboot") } 115 | ) { 116 | Text(stringResource(R.string.reboot)) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /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.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.setValue 11 | import androidx.lifecycle.ViewModel 12 | import androidx.lifecycle.viewModelScope 13 | import androidx.navigation.NavController 14 | import com.github.capntrips.kernelflasher.common.PartitionUtil 15 | import com.github.capntrips.kernelflasher.common.types.backups.Backup 16 | import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsViewModel 17 | import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootViewModel 18 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel 19 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewModel 20 | import com.topjohnwu.superuser.Shell 21 | import com.topjohnwu.superuser.nio.FileSystemManager 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import kotlinx.coroutines.withContext 25 | import kotlinx.serialization.ExperimentalSerializationApi 26 | import java.io.File 27 | import java.time.LocalDateTime 28 | import java.time.format.DateTimeFormatter 29 | 30 | @ExperimentalSerializationApi 31 | class MainViewModel( 32 | context: Context, 33 | fileSystemManager: FileSystemManager, 34 | private val navController: NavController 35 | ) : ViewModel() { 36 | companion object { 37 | const val TAG: String = "KernelFlasher/MainViewModel" 38 | } 39 | val slotSuffix: String 40 | 41 | val kernelVersion: String 42 | val susfsVersion: String 43 | val isAb: Boolean 44 | val slotA: SlotViewModel 45 | val slotB: SlotViewModel? 46 | val backups: BackupsViewModel 47 | val updates: UpdatesViewModel 48 | val reboot: RebootViewModel 49 | val hasRamoops: Boolean 50 | 51 | private val _isRefreshing: MutableState = mutableStateOf(true) 52 | private var _error: String? = null 53 | private var _backups: MutableMap = mutableMapOf() 54 | 55 | val isRefreshing: Boolean 56 | get() = _isRefreshing.value 57 | val hasError: Boolean 58 | get() = _error != null 59 | val error: String 60 | get() = _error!! 61 | 62 | data class UpdateDialogData( 63 | val title: String, 64 | val changelog: List, 65 | val onConfirm: () -> Unit 66 | ) 67 | 68 | var updateDialogData by mutableStateOf(null) 69 | private set 70 | 71 | fun showUpdateDialog(title: String, changelog: List, onConfirm: () -> Unit) { 72 | updateDialogData = UpdateDialogData(title, changelog, onConfirm) 73 | } 74 | 75 | fun hideUpdateDialog() { 76 | updateDialogData = null 77 | } 78 | 79 | init { 80 | PartitionUtil.init(context, fileSystemManager) 81 | kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0] 82 | susfsVersion = runCatching { Shell.cmd("susfsd version").exec().out[0] } 83 | .recoverCatching { Shell.cmd("ksu_susfs show version").exec().out[0] } 84 | .getOrDefault("v0.0.0") 85 | slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] 86 | backups = BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups) 87 | updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing) 88 | reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing) 89 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:bootable/recovery/recovery.cpp;l=320 90 | isAb = slotSuffix.isNotEmpty() 91 | if (isAb) { 92 | val bootA = PartitionUtil.findPartitionBlockDevice(context, "boot", "_a")!! 93 | val bootB = PartitionUtil.findPartitionBlockDevice(context, "boot", "_b")!! 94 | val initBootA = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_a") 95 | val initBootB = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_b") 96 | slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_a", "_a", bootA, initBootA, _backups) 97 | slotB = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_b", "_b", bootB, initBootB, _backups) 98 | } else { 99 | val boot = PartitionUtil.findPartitionBlockDevice(context, "boot", "")!! 100 | val initBoot = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "") 101 | slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, true, "", boot, initBoot, _backups) 102 | if (slotA.hasError) { 103 | _error = slotA.error 104 | } 105 | slotB = null 106 | } 107 | 108 | hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists() 109 | _isRefreshing.value = false 110 | } 111 | 112 | fun refresh(context: Context) { 113 | launch { 114 | slotA.refresh(context) 115 | if (isAb) { 116 | slotB!!.refresh(context) 117 | } 118 | backups.refresh(context) 119 | } 120 | } 121 | 122 | private fun launch(block: suspend () -> Unit) { 123 | viewModelScope.launch(Dispatchers.IO) { 124 | viewModelScope.launch(Dispatchers.Main) { 125 | _isRefreshing.value = true 126 | } 127 | try { 128 | block() 129 | } catch (e: Exception) { 130 | withContext (Dispatchers.Main) { 131 | Log.e(TAG, e.message, e) 132 | navController.navigate("error/${e.message}") { 133 | popUpTo("main") 134 | } 135 | } 136 | } 137 | viewModelScope.launch(Dispatchers.Main) { 138 | _isRefreshing.value = false 139 | } 140 | } 141 | } 142 | 143 | @Suppress("SameParameterValue") 144 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 145 | Log.d(TAG, message) 146 | if (!shouldThrow) { 147 | viewModelScope.launch(Dispatchers.Main) { 148 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 149 | } 150 | } else { 151 | throw Exception(message) 152 | } 153 | } 154 | 155 | fun saveRamoops(context: Context) { 156 | launch { 157 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 158 | @SuppressLint("SdCardPath") 159 | val ramoops = File("/sdcard/Download/console-ramoops--$now.log") 160 | Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec() 161 | if (ramoops.exists()) { 162 | log(context, "Saved ramoops to $ramoops") 163 | } else { 164 | log(context, "Failed to save $ramoops", shouldThrow = true) 165 | } 166 | } 167 | } 168 | 169 | fun saveDmesg(context: Context) { 170 | launch { 171 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 172 | @SuppressLint("SdCardPath") 173 | val dmesg = File("/sdcard/Download/dmesg--$now.log") 174 | Shell.cmd("dmesg > $dmesg").exec() 175 | if (dmesg.exists()) { 176 | log(context, "Saved dmesg to $dmesg") 177 | } else { 178 | log(context, "Failed to save $dmesg", shouldThrow = true) 179 | } 180 | } 181 | } 182 | 183 | fun saveLogcat(context: Context) { 184 | launch { 185 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 186 | @SuppressLint("SdCardPath") 187 | val logcat = File("/sdcard/Download/logcat--$now.log") 188 | Shell.cmd("logcat -d > $logcat").exec() 189 | if (logcat.exists()) { 190 | log(context, "Saved logcat to $logcat") 191 | } else { 192 | log(context, "Failed to save $logcat", shouldThrow = true) 193 | } 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /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.Build 4 | import android.os.PowerManager 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.OutlinedButton 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.unit.dp 15 | import androidx.navigation.NavController 16 | import com.github.capntrips.kernelflasher.R 17 | 18 | @Suppress("UnusedReceiverParameter") 19 | @Composable 20 | fun ColumnScope.RebootContent( 21 | viewModel: RebootViewModel, 22 | @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController 23 | ) { 24 | val context = LocalContext.current 25 | OutlinedButton( 26 | modifier = Modifier 27 | .fillMaxWidth(), 28 | shape = RoundedCornerShape(4.dp), 29 | onClick = { viewModel.rebootSystem() } 30 | ) { 31 | Text(stringResource(R.string.reboot)) 32 | } 33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && context.getSystemService(PowerManager::class.java)?.isRebootingUserspaceSupported == true) { 34 | OutlinedButton( 35 | modifier = Modifier 36 | .fillMaxWidth(), 37 | shape = RoundedCornerShape(4.dp), 38 | onClick = { viewModel.rebootUserspace() } 39 | ) { 40 | Text(stringResource(R.string.reboot_userspace)) 41 | } 42 | } 43 | OutlinedButton( 44 | modifier = Modifier 45 | .fillMaxWidth(), 46 | shape = RoundedCornerShape(4.dp), 47 | onClick = { viewModel.rebootRecovery() } 48 | ) { 49 | Text(stringResource(R.string.reboot_recovery)) 50 | } 51 | OutlinedButton( 52 | modifier = Modifier 53 | .fillMaxWidth(), 54 | shape = RoundedCornerShape(4.dp), 55 | onClick = { viewModel.rebootBootloader() } 56 | ) { 57 | Text(stringResource(R.string.reboot_bootloader)) 58 | } 59 | OutlinedButton( 60 | modifier = Modifier 61 | .fillMaxWidth(), 62 | shape = RoundedCornerShape(4.dp), 63 | onClick = { viewModel.rebootDownload() } 64 | ) { 65 | Text(stringResource(R.string.reboot_download)) 66 | } 67 | OutlinedButton( 68 | modifier = Modifier 69 | .fillMaxWidth(), 70 | shape = RoundedCornerShape(4.dp), 71 | onClick = { viewModel.rebootEdl() } 72 | ) { 73 | Text(stringResource(R.string.reboot_edl)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /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").submit() 53 | } 54 | } 55 | 56 | fun rebootSystem() { 57 | reboot() 58 | } 59 | 60 | fun rebootUserspace() { 61 | reboot("userspace") 62 | } 63 | 64 | fun rebootRecovery() { 65 | reboot("recovery") 66 | } 67 | 68 | fun rebootBootloader() { 69 | reboot("bootloader") 70 | } 71 | 72 | fun rebootDownload() { 73 | reboot("download") 74 | } 75 | 76 | fun rebootEdl() { 77 | reboot("edl") 78 | } 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.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 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.ExperimentalUnitApi 19 | import androidx.compose.ui.unit.dp 20 | import androidx.navigation.NavController 21 | import com.github.capntrips.kernelflasher.R 22 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 23 | 24 | @ExperimentalAnimationApi 25 | @ExperimentalMaterial3Api 26 | @ExperimentalUnitApi 27 | @Composable 28 | fun ColumnScope.SlotContent( 29 | viewModel: SlotViewModel, 30 | slotSuffix: String, 31 | navController: NavController 32 | ) { 33 | val context = LocalContext.current 34 | SlotCard( 35 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), 36 | viewModel = viewModel, 37 | navController = navController, 38 | isSlotScreen = true 39 | ) 40 | AnimatedVisibility(!viewModel.isRefreshing.value) { 41 | Column { 42 | Spacer(Modifier.height(5.dp)) 43 | OutlinedButton( 44 | modifier = Modifier 45 | .fillMaxWidth(), 46 | shape = RoundedCornerShape(4.dp), 47 | onClick = { 48 | navController.navigate("slot$slotSuffix/flash") 49 | } 50 | ) { 51 | Text(stringResource(R.string.flash)) 52 | } 53 | OutlinedButton( 54 | modifier = Modifier 55 | .fillMaxWidth(), 56 | shape = RoundedCornerShape(4.dp), 57 | onClick = { 58 | viewModel.clearFlash(context) 59 | navController.navigate("slot$slotSuffix/backup") 60 | } 61 | ) { 62 | Text(stringResource(R.string.backup)) 63 | } 64 | OutlinedButton( 65 | modifier = Modifier 66 | .fillMaxWidth(), 67 | shape = RoundedCornerShape(4.dp), 68 | onClick = { 69 | navController.navigate("slot$slotSuffix/backups") 70 | } 71 | ) { 72 | Text(stringResource(R.string.restore)) 73 | } 74 | OutlinedButton( 75 | modifier = Modifier 76 | .fillMaxWidth(), 77 | shape = RoundedCornerShape(4.dp), 78 | onClick = { if (!viewModel.isRefreshing.value) viewModel.getKernel(context) } 79 | ) { 80 | Text(stringResource(R.string.check_kernel_version)) 81 | } 82 | if (viewModel.hasVendorDlkm) { 83 | AnimatedVisibility(!viewModel.isRefreshing.value) { 84 | AnimatedVisibility(viewModel.isVendorDlkmMounted) { 85 | OutlinedButton( 86 | modifier = Modifier 87 | .fillMaxWidth(), 88 | shape = RoundedCornerShape(4.dp), 89 | onClick = { viewModel.unmountVendorDlkm(context) } 90 | ) { 91 | Text(stringResource(R.string.unmount_vendor_dlkm)) 92 | } 93 | } 94 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) { 95 | Column { 96 | OutlinedButton( 97 | modifier = Modifier 98 | .fillMaxWidth(), 99 | shape = RoundedCornerShape(4.dp), 100 | onClick = { viewModel.mountVendorDlkm(context) } 101 | ) { 102 | Text(stringResource(R.string.mount_vendor_dlkm)) 103 | } 104 | OutlinedButton( 105 | modifier = Modifier 106 | .fillMaxWidth(), 107 | shape = RoundedCornerShape(4.dp), 108 | onClick = { viewModel.unmapVendorDlkm(context) } 109 | ) { 110 | Text(stringResource(R.string.unmap_vendor_dlkm)) 111 | } 112 | } 113 | } 114 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) { 115 | OutlinedButton( 116 | modifier = Modifier 117 | .fillMaxWidth(), 118 | shape = RoundedCornerShape(4.dp), 119 | onClick = { viewModel.mapVendorDlkm(context) } 120 | ) { 121 | Text(stringResource(R.string.map_vendor_dlkm)) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /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.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.ColumnScope 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.offset 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.foundation.layout.Arrangement 15 | import androidx.compose.foundation.layout.padding 16 | import androidx.compose.material.ExperimentalMaterialApi 17 | import androidx.compose.material3.ButtonDefaults 18 | import androidx.compose.material3.Checkbox 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.MaterialTheme 21 | import androidx.compose.material3.OutlinedButton 22 | import androidx.compose.material3.Text 23 | import androidx.compose.material3.AlertDialog 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.unit.ExperimentalUnitApi 35 | import androidx.compose.ui.unit.dp 36 | import androidx.compose.ui.text.font.FontWeight 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.FlashButton 42 | import com.github.capntrips.kernelflasher.ui.components.FlashList 43 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 44 | import com.github.capntrips.kernelflasher.ui.components.DialogButton 45 | import kotlinx.serialization.ExperimentalSerializationApi 46 | 47 | @ExperimentalAnimationApi 48 | @ExperimentalMaterialApi 49 | @ExperimentalMaterial3Api 50 | @ExperimentalUnitApi 51 | @ExperimentalSerializationApi 52 | @Composable 53 | fun ColumnScope.SlotFlashContent( 54 | viewModel: SlotViewModel, 55 | slotSuffix: String, 56 | navController: NavController 57 | ) { 58 | val context = LocalContext.current 59 | 60 | val isRefreshing by remember { derivedStateOf { viewModel.isRefreshing } } 61 | val currentRoute = navController.currentDestination!!.route.orEmpty() 62 | 63 | BackHandler(enabled = ((currentRoute.endsWith("/flash/ak3") || 64 | currentRoute.endsWith("/flash/image/flash") || 65 | currentRoute.endsWith("/backup/backup")) && isRefreshing.value)) { 66 | 67 | } 68 | 69 | if (!listOf("/flash/ak3", "/flash/image/flash", "/backup/backup").any { navController.currentDestination!!.route!!.endsWith(it) }) { 70 | SlotCard( 71 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), 72 | viewModel = viewModel, 73 | navController = navController, 74 | isSlotScreen = true, 75 | showDlkm = false 76 | ) 77 | Spacer(Modifier.height(16.dp)) 78 | if (navController.currentDestination!!.route!!.endsWith("/flash")) { 79 | DataCard (stringResource(R.string.flash)) 80 | Spacer(Modifier.height(5.dp)) 81 | FlashButton(stringResource(R.string.flash_ak3_zip), "zip" ,callback = { uri -> 82 | navController.navigate("slot$slotSuffix/flash/ak3") { 83 | popUpTo("slot$slotSuffix") 84 | } 85 | viewModel.flashAk3(context, uri) 86 | }) 87 | FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip" ,callback = { uri -> 88 | navController.navigate("slot$slotSuffix/flash/ak3") { 89 | popUpTo("slot$slotSuffix") 90 | } 91 | viewModel.flashAk3_mkbootfs(context, uri) 92 | }) 93 | FlashButton(stringResource(R.string.flash_ksu_lkm), "ko" ,callback = { uri -> 94 | navController.navigate("slot$slotSuffix/flash/image/flash") { 95 | popUpTo("slot$slotSuffix") 96 | } 97 | viewModel.flashKsuDriver(context, uri) 98 | }) 99 | OutlinedButton( 100 | modifier = Modifier 101 | .fillMaxWidth(), 102 | shape = RoundedCornerShape(4.dp), 103 | onClick = { 104 | navController.navigate("slot$slotSuffix/flash/image") 105 | } 106 | ) { 107 | Text(stringResource(R.string.flash_partition_image)) 108 | } 109 | } else if (navController.currentDestination!!.route!!.endsWith("/flash/image")) { 110 | DataCard (stringResource(R.string.flash_partition_image)) 111 | Spacer(Modifier.height(5.dp)) 112 | for (partitionName in PartitionUtil.AvailablePartitions) { 113 | FlashButton(partitionName, "img" ,callback = { uri -> 114 | navController.navigate("slot$slotSuffix/flash/image/flash") { 115 | popUpTo("slot$slotSuffix") 116 | } 117 | viewModel.flashImage(context, uri, partitionName) 118 | }) 119 | } 120 | } else if (navController.currentDestination!!.route!!.endsWith("/backup")) { 121 | DataCard (stringResource(R.string.backup)) 122 | Spacer(Modifier.height(5.dp)) 123 | val disabledColor = ButtonDefaults.buttonColors( 124 | Color.Transparent, 125 | MaterialTheme.colorScheme.onSurface 126 | ) 127 | for (partitionName in PartitionUtil.AvailablePartitions) { 128 | OutlinedButton( 129 | modifier = Modifier 130 | .fillMaxWidth() 131 | .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), 132 | shape = RoundedCornerShape(4.dp), 133 | colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor, 134 | onClick = { 135 | viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!! 136 | }, 137 | ) { 138 | Box(Modifier.fillMaxWidth()) { 139 | Checkbox(viewModel.backupPartitions[partitionName]!!, null, 140 | Modifier 141 | .align(Alignment.CenterStart) 142 | .offset(x = -(16.dp))) 143 | Text(partitionName, Modifier.align(Alignment.Center)) 144 | } 145 | } 146 | } 147 | OutlinedButton( 148 | modifier = Modifier 149 | .fillMaxWidth(), 150 | shape = RoundedCornerShape(4.dp), 151 | onClick = { 152 | viewModel.backup(context) 153 | navController.navigate("slot$slotSuffix/backup/backup") { 154 | popUpTo("slot$slotSuffix") 155 | } 156 | }, 157 | enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty() 158 | ) { 159 | Text(stringResource(R.string.backup_now)) 160 | } 161 | } 162 | } else { 163 | Text("") 164 | FlashList( 165 | stringResource(if (navController.currentDestination!!.route!!.endsWith("/backup/backup")) R.string.backup else R.string.flash), 166 | if (navController.currentDestination!!.route!!.contains("ak3")) viewModel.uiPrintedOutput else viewModel.flashOutput 167 | ) { 168 | AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.wasFlashSuccess.value != null) { 169 | Column { 170 | if (navController.currentDestination!!.route!!.contains("ak3")) { 171 | OutlinedButton( 172 | modifier = Modifier 173 | .fillMaxWidth(), 174 | shape = RoundedCornerShape(4.dp), 175 | onClick = { viewModel.saveLog(context) } 176 | ) { 177 | if (navController.currentDestination!!.route!!.contains("ak3")) { 178 | Text(stringResource(R.string.save_ak3_log)) 179 | } else if (navController.currentDestination!!.route!!.endsWith("/backup/backup")) { 180 | Text(stringResource(R.string.save_backup_log)) 181 | } else { 182 | Text(stringResource(R.string.save_flash_log)) 183 | } 184 | } 185 | } 186 | if (navController.currentDestination!!.route!!.contains("ak3")) { 187 | AnimatedVisibility(!navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/flash/ak3") && viewModel.wasFlashSuccess.value != false) { 188 | OutlinedButton( 189 | modifier = Modifier 190 | .fillMaxWidth(), 191 | shape = RoundedCornerShape(4.dp), 192 | onClick = { 193 | viewModel.backupZip(context) { 194 | navController.navigate("slot$slotSuffix/backups") { 195 | popUpTo("slot$slotSuffix") 196 | } 197 | } 198 | } 199 | ) { 200 | Text(stringResource(R.string.save_ak3_zip_as_backup)) 201 | } 202 | } 203 | } 204 | if (viewModel.wasFlashSuccess.value == true && viewModel.showCautionDialog == true){ 205 | AlertDialog( 206 | onDismissRequest = { viewModel.hideCautionDialog() }, 207 | title = { Text("CAUTION!", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) }, 208 | text = { 209 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { 210 | Text("You have flashed to inactive slot!", fontWeight = FontWeight.Bold) 211 | Text("But the active slot is not changed after flashing.", fontWeight = FontWeight.Bold) 212 | Text("Change active slot or return to System Updater to complete OTA.", fontWeight = FontWeight.Bold) 213 | Text("Do not reboot from here, unless you know what you are doing.", fontWeight = FontWeight.Bold) 214 | } 215 | }, 216 | confirmButton = { 217 | DialogButton( 218 | "CHANGE SLOT", 219 | { 220 | viewModel.hideCautionDialog() 221 | viewModel.switchSlot(context) 222 | } 223 | ) 224 | }, 225 | dismissButton = { 226 | DialogButton( 227 | "CANCEL", 228 | { 229 | viewModel.hideCautionDialog() 230 | } 231 | ) 232 | }, 233 | modifier = Modifier.padding(16.dp) 234 | ) 235 | } 236 | if (viewModel.wasFlashSuccess.value != false && navController.currentDestination!!.route!!.endsWith("/backup/backup")) { 237 | OutlinedButton( 238 | modifier = Modifier 239 | .fillMaxWidth(), 240 | shape = RoundedCornerShape(4.dp), 241 | onClick = { navController.popBackStack() } 242 | ) { 243 | Text(stringResource(R.string.back)) 244 | } 245 | } else { 246 | OutlinedButton( 247 | modifier = Modifier 248 | .fillMaxWidth(), 249 | shape = RoundedCornerShape(4.dp), 250 | onClick = { navController.navigate("reboot") } 251 | ) { 252 | Text(stringResource(R.string.reboot)) 253 | } 254 | } 255 | } 256 | } 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /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.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.material3.OutlinedButton 10 | import androidx.compose.material3.OutlinedTextField 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.unit.dp 21 | import androidx.navigation.NavController 22 | import com.github.capntrips.kernelflasher.R 23 | import kotlinx.serialization.ExperimentalSerializationApi 24 | 25 | @Suppress("UnusedReceiverParameter") 26 | @ExperimentalMaterial3Api 27 | @ExperimentalSerializationApi 28 | @Composable 29 | fun ColumnScope.UpdatesAddContent( 30 | viewModel: UpdatesViewModel, 31 | navController: NavController 32 | ) { 33 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current 34 | var url by remember { mutableStateOf("") } 35 | OutlinedTextField( 36 | value = url, 37 | onValueChange = { url = it }, 38 | label = { Text(stringResource(R.string.url)) }, 39 | modifier = Modifier 40 | .fillMaxWidth() 41 | ) 42 | Spacer(Modifier.height(5.dp)) 43 | OutlinedButton( 44 | modifier = Modifier 45 | .fillMaxWidth(), 46 | shape = RoundedCornerShape(4.dp), 47 | onClick = { viewModel.add(url) { navController.navigate("updates/view/$it") { popUpTo("updates") } } } 48 | ) { 49 | Text(stringResource(R.string.add)) 50 | } 51 | } -------------------------------------------------------------------------------- /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(viewModel.changelog!!, 33 | style = LocalTextStyle.current.copy( 34 | fontFamily = FontFamily.Monospace, 35 | fontSize = TextUnit(12.0f, TextUnitType.Sp), 36 | lineHeight = TextUnit(18.0f, TextUnitType.Sp) 37 | ) 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /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.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.font.FontStyle 21 | import androidx.compose.ui.unit.dp 22 | import androidx.navigation.NavController 23 | import com.github.capntrips.kernelflasher.R 24 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer 25 | import com.github.capntrips.kernelflasher.ui.components.DataCard 26 | import com.github.capntrips.kernelflasher.ui.components.DataRow 27 | import com.github.capntrips.kernelflasher.ui.components.ViewButton 28 | import kotlinx.serialization.ExperimentalSerializationApi 29 | 30 | @ExperimentalMaterial3Api 31 | @ExperimentalSerializationApi 32 | @Composable 33 | fun ColumnScope.UpdatesContent( 34 | viewModel: UpdatesViewModel, 35 | navController: NavController 36 | ) { 37 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current 38 | DataCard(stringResource(R.string.updates)) 39 | if (viewModel.updates.isNotEmpty()) { 40 | for (update in viewModel.updates.sortedByDescending { it.kernelDate }) { 41 | Spacer(Modifier.height(16.dp)) 42 | DataCard( 43 | title = update.kernelName, 44 | button = { 45 | AnimatedVisibility(!viewModel.isRefreshing) { 46 | Column { 47 | ViewButton(onClick = { 48 | navController.navigate("updates/view/${update.id}") 49 | }) 50 | } 51 | } 52 | } 53 | ) { 54 | val cardWidth = remember { mutableIntStateOf(0) } 55 | DataRow(stringResource(R.string.version), update.kernelVersion, mutableMaxWidth = cardWidth) 56 | DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(update.kernelDate), mutableMaxWidth = cardWidth) 57 | DataRow( 58 | label = stringResource(R.string.last_updated), 59 | value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!), 60 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 61 | labelStyle = MaterialTheme.typography.labelMedium.copy( 62 | fontStyle = FontStyle.Italic 63 | ), 64 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 65 | valueStyle = MaterialTheme.typography.titleSmall.copy( 66 | fontStyle = FontStyle.Italic 67 | ), 68 | mutableMaxWidth = cardWidth 69 | ) 70 | } 71 | } 72 | } 73 | AnimatedVisibility(!viewModel.isRefreshing) { 74 | Column { 75 | Spacer(Modifier.height(12.dp)) 76 | OutlinedButton( 77 | modifier = Modifier 78 | .fillMaxWidth(), 79 | shape = RoundedCornerShape(4.dp), 80 | onClick = { navController.navigate("updates/add") } 81 | ) { 82 | Text(stringResource(R.string.add)) 83 | } 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /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.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material3.ExperimentalMaterial3Api 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.mutableIntStateOf 16 | import androidx.compose.runtime.remember 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.platform.LocalContext 19 | import androidx.compose.ui.res.stringResource 20 | import androidx.compose.ui.text.font.FontStyle 21 | import androidx.compose.ui.unit.dp 22 | import androidx.navigation.NavController 23 | import com.github.capntrips.kernelflasher.R 24 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer 25 | import com.github.capntrips.kernelflasher.ui.components.DataCard 26 | import com.github.capntrips.kernelflasher.ui.components.DataRow 27 | import kotlinx.serialization.ExperimentalSerializationApi 28 | 29 | @ExperimentalMaterial3Api 30 | @ExperimentalSerializationApi 31 | @Composable 32 | fun ColumnScope.UpdatesViewContent( 33 | viewModel: UpdatesViewModel, 34 | navController: NavController 35 | ) { 36 | val context = LocalContext.current 37 | viewModel.currentUpdate?.let { currentUpdate -> 38 | DataCard(currentUpdate.kernelName) { 39 | val cardWidth = remember { mutableIntStateOf(0) } 40 | DataRow(stringResource(R.string.version), currentUpdate.kernelVersion, mutableMaxWidth = cardWidth) 41 | DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(currentUpdate.kernelDate), mutableMaxWidth = cardWidth) 42 | DataRow( 43 | label = stringResource(R.string.last_updated), 44 | value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!), 45 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 46 | labelStyle = MaterialTheme.typography.labelMedium.copy( 47 | fontStyle = FontStyle.Italic 48 | ), 49 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 50 | valueStyle = MaterialTheme.typography.titleSmall.copy( 51 | fontStyle = FontStyle.Italic, 52 | ), 53 | mutableMaxWidth = cardWidth 54 | ) 55 | } 56 | AnimatedVisibility(!viewModel.isRefreshing) { 57 | Column { 58 | Spacer(Modifier.height(5.dp)) 59 | OutlinedButton( 60 | modifier = Modifier 61 | .fillMaxWidth(), 62 | shape = RoundedCornerShape(4.dp), 63 | onClick = { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } } 64 | ) { 65 | Text(stringResource(R.string.changelog)) 66 | } 67 | // TODO: add download progress indicator 68 | OutlinedButton( 69 | modifier = Modifier 70 | .fillMaxWidth(), 71 | shape = RoundedCornerShape(4.dp), 72 | onClick = { viewModel.downloadKernel(context) } 73 | ) { 74 | Text(stringResource(R.string.download)) 75 | } 76 | OutlinedButton( 77 | modifier = Modifier 78 | .fillMaxWidth(), 79 | shape = RoundedCornerShape(4.dp), 80 | onClick = { viewModel.update() } 81 | ) { 82 | Text(stringResource(R.string.check_for_updates)) 83 | } 84 | OutlinedButton( 85 | modifier = Modifier 86 | .fillMaxWidth(), 87 | shape = RoundedCornerShape(4.dp), 88 | onClick = { viewModel.delete { navController.popBackStack() } } 89 | ) { 90 | Text(stringResource(R.string.delete)) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /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 android.os.Build 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.dynamicDarkColorScheme 8 | import androidx.compose.material3.dynamicLightColorScheme 9 | import androidx.compose.material3.lightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.platform.LocalContext 12 | 13 | @Composable 14 | fun KernelFlasherTheme( 15 | darkTheme: Boolean = isSystemInDarkTheme(), 16 | dynamicColor: Boolean = true, 17 | content: @Composable () -> Unit 18 | ) { 19 | val colorScheme = when { 20 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 21 | val context = LocalContext.current 22 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 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/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/jniLibs/arm64-v8a/libhttools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/liblptools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/jniLibs/arm64-v8a/liblptools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/arm64-v8a/libmagiskboot.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libhttools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/jniLibs/armeabi-v7a/libhttools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/liblptools_static.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/app/src/main/jniLibs/armeabi-v7a/liblptools_static.so -------------------------------------------------------------------------------- /app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/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-it/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Kernel Flasher 3 | È richiesta il root 4 | Servizio root disconnesso 5 | Dispositivo 6 | Modello 7 | Numero build 8 | Nome kernel 9 | Versione kernel 10 | Suffisso slot 11 | Slot 12 | Slot A 13 | Slot B 14 | Boot SHA1 15 | Vendor DLKM 16 | Esiste 17 | Non trovato 18 | Montato 19 | Smontato 20 | Visualizza 21 | Backups 22 | Salva ramoops 23 | Salva dmesg 24 | Salva logcat 25 | Indietro 26 | Backup 27 | Aggiornamenti 28 | Flash 29 | Flash utilizzando mkbootfs 30 | Flash AK3 Zip 31 | Flash AK3 Zip utilizzando mkbootfs 32 | Flash Partitione Image 33 | Flash KernelSU LKM Driver 34 | Ripristina 35 | Controlla versione kernel 36 | Monta Vendor DLKM 37 | Smonta Vendor DLKM 38 | Mappa Vendor DLKM 39 | Rimuovi mappa Vendor DLKM 40 | Migrare 41 | Nessun backup trovato 42 | Elimina 43 | Aggiungi 44 | URL 45 | Versione 46 | Data di rilascio 47 | Ultimo aggiornamento 48 | Changelog 49 | Controlla gli aggiornamenti 50 | Download 51 | Riavvio 52 | Riavvio Veloce 53 | Riavvio in Recovery 54 | Riavvio in Bootloader 55 | Riavvio in Download 56 | Riavvio in EDL 57 | Salva AK3 Log 58 | Salva Flash Log 59 | Salva Backup Log 60 | Salva Restore Log 61 | Salva AK3 Zip come backup 62 | Tipo Backup 63 | Hashes 64 | Selezione della partizione non disponibile per il backup legacy 65 | Formato boot.img 66 | Formato init_boot.img 67 | Formato Ramdisk 68 | Versione SUSFS 69 | Backup Partizioni Selezionate 70 | 71 | 72 | -------------------------------------------------------------------------------- /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 | 選択したパーティションをバックアップする 71 | 72 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | -------------------------------------------------------------------------------- /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 | Utwórz kopię zapasową wybranych partycji 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 | Fazer backup das partições selecionadas 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/values-ru/strings.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 | 71 | -------------------------------------------------------------------------------- /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 | 插槽 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/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 | Backup Selected Partitions 70 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 15 | -------------------------------------------------------------------------------- /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 | } 8 | 9 | tasks.register('clean', Delete) { 10 | delete rootProject.buildDir 11 | } -------------------------------------------------------------------------------- /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 24 | 25 | android.defaults.buildfeatures.buildconfig=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.21" 3 | compose-compiler = "1.5.6" 4 | 5 | androidx-activity-compose = "1.10.1" 6 | androidx-appcompat = "1.7.0" 7 | androidx-compose = "1.8.1" 8 | androidx-compose-material3 = "1.3.2" 9 | androidx-core-ktx = "1.16.0" 10 | androidx-core-splashscreen = "1.0.1" 11 | androidx-lifecycle = "2.9.0" 12 | androidx-navigation-compose = "2.9.0" 13 | androidx-room = "2.7.1" 14 | kotlinx-serialization-json = "1.8.1" 15 | libsu = "6.0.0" 16 | material = "1.12.0" 17 | okhttp = "4.12.0" 18 | 19 | android-application = "8.10.0" 20 | devtools-ksp = "2.1.20-2.0.1" 21 | 22 | [libraries] 23 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" } 24 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } 25 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } 26 | androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose" } 27 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } 28 | androidx-compose-ui = { group = "androidx.compose.ui", name="ui", version.ref = "androidx-compose" } 29 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } 30 | androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" } 31 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } 32 | androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } 33 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" } 34 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } 35 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" } 36 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } 37 | libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } 38 | libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } 39 | libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" } 40 | libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } 41 | material = { group = "com.google.android.material", name = "material", version.ref = "material" } 42 | okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } 43 | 44 | [plugins] 45 | android-application = { id = "com.android.application", version.ref = "android-application" } 46 | devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" } 47 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 48 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 49 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 50 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatalcoder524/KernelFlasher/819ed7ed34855cca4e7fa7f5d24531621754556b/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 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { url 'https://jitpack.io' } 14 | } 15 | } 16 | rootProject.name = "Kernel Flasher" 17 | include ':app' 18 | --------------------------------------------------------------------------------