Repository: fatalcoder524/KernelFlasher Branch: allow-errors Commit: bfa337727d54 Files: 90 Total size: 288.0 KB Directory structure: gitextract_gnutdixd/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── com.github.capntrips.kernelflasher.common.types.room.AppDatabase/ │ │ └── 1.json │ └── 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 │ └── 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 │ ├── resources.properties │ ├── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.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 │ └── xml/ │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gradle" directory: "/" target-branch: "allow-errors" schedule: interval: "daily" ================================================ FILE: .github/workflows/build.yml ================================================ name: Android Build permissions: contents: write on: workflow_dispatch: push: pull_request: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # Important: get all tags! - name: Get Latest Tag run: | latest_tag=$(git describe --tags --abbrev=0) echo "LATEST_TAG=${latest_tag}" >> $GITHUB_ENV - name: Count Commits Since Latest Tag run: | # Get the number of commits since the latest tag commits_since_tag=$(git rev-list ${LATEST_TAG}..HEAD --count) echo "COMMITS_SINCE_TAG=${commits_since_tag}" >> $GITHUB_ENV - name: Generate Version run: | # Extract version components version_base="${LATEST_TAG#v}" major=$(echo $version_base | cut -d. -f1) minor=$(echo $version_base | cut -d. -f2) patch=$(echo $version_base | cut -d. -f3) # Increment patch by the number of commits since the last tag new_patch=$((patch + $COMMITS_SINCE_TAG)) # Construct the new version string new_version="$major.$minor.$new_patch" # Calculate the version code (e.g., 1.0.5 -> 10005) version_code=$((major * 10000 + minor * 100 + new_patch)) # Set environment variables echo "NEW_VERSION_NAME=${new_version}" >> $GITHUB_ENV echo "NEW_VERSION_CODE=${version_code}" >> $GITHUB_ENV - name: Update build.gradle.kts with new version code and version name run: | # Update versionCode and versionName in build.gradle sed -i "s/versionCode [0-9]\+/versionCode ${NEW_VERSION_CODE}/" app/build.gradle sed -i "s/versionName \"[^\"]*\"/versionName \"${NEW_VERSION_NAME}\"/" app/build.gradle - name: Print Version run: | echo "Version Name: $NEW_VERSION_NAME" echo "Version Code: $NEW_VERSION_CODE" - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: "temurin" java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle run: | chmod +x ./gradlew ./gradlew assembleRelease tree app/build/outputs/apk/release - uses: r0adkll/sign-android-release@v1.0.4 name: Sign app APK if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'workflow_dispatch' id: sign_app with: releaseDirectory: app/build/outputs/apk/release signingKeyBase64: ${{ secrets.KEYSTORE }} alias: ${{ secrets.KEY_ALIAS }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: BUILD_TOOLS_VERSION: "35.0.0" - name: Rename APK if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'workflow_dispatch' run: | ls -al app/build/outputs/apk/release echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}" cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk - name: Rename APK if: github.repository != github.event.pull_request.head.repo.full_name && github.event_name == 'pull_request' run: | ls -al app/build/outputs/apk/release cp ./app/build/outputs/apk/release/app-release-unsigned.apk KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk - name: Upload APK uses: actions/upload-artifact@v4.3.5 with: name: KernelFlasher_${{ env.NEW_VERSION_CODE }} path: KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk # - name: Rename apk # run: | # ls -al # DATE=$(date +'%y.%m.%d') # echo "TAG=$DATE" >> $GITHUB_ENV # - name: Upload release # uses: ncipollo/release-action@v1.14.0 # with: # allowUpdates: true # removeArtifacts: true # name: "1.${{ github.run_number }}.0" # tag: "v1.${{ github.run_number }}.0" # body: | # Note: QMod KernelFlasher, support ksu-lkm # artifacts: "*.apk" ================================================ FILE: .github/workflows/publish.yml ================================================ name: Android Release permissions: contents: write on: workflow_dispatch: inputs: increment_major: description: 'Increment Major Version by?' required: true default: 0 type: number increment_minor: description: 'Increment Minor Version by?' required: true default: 0 type: number increment_patch: description: 'Increment Patch Version by?' required: true default: 0 type: number changes_in_release: description: 'Changes in release' required: true default: 'Minor changes' type: string jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 0 # Important: get all tags! - name: Get Latest Tag run: | latest_tag=$(git describe --tags --abbrev=0) echo "LATEST_TAG=${latest_tag}" >> $GITHUB_ENV - name: Generate Version run: | # Extract version components version_base="${LATEST_TAG#v}" major=$(echo $version_base | cut -d. -f1) minor=$(echo $version_base | cut -d. -f2) patch=$(echo $version_base | cut -d. -f3) # Increment versions by the specified input values new_major=$((major + ${{ github.event.inputs.increment_major }})) if [ ${{ github.event.inputs.increment_major }} -gt 0 ]; then new_minor="${{ github.event.inputs.increment_minor }}" new_patch="${{ github.event.inputs.increment_patch }}" else new_minor=$((minor + ${{ github.event.inputs.increment_minor }})) if [ ${{ github.event.inputs.increment_minor }} -gt 0 ]; then new_patch="${{ github.event.inputs.increment_patch }}" else new_patch=$((patch + ${{ github.event.inputs.increment_patch }})) fi fi # Construct the new version string new_version="$new_major.$new_minor.$new_patch" # Calculate the version code (e.g., 1.0.5 -> 10005) version_code=$((new_major * 10000 + new_minor * 100 + new_patch)) # Set environment variables echo "NEW_VERSION_NAME=${new_version}" >> $GITHUB_ENV echo "NEW_VERSION_CODE=${version_code}" >> $GITHUB_ENV - name: Update build.gradle.kts with new version code and version name run: | # Update versionCode and versionName in build.gradle sed -i "s/versionCode [0-9]\+/versionCode ${NEW_VERSION_CODE}/" app/build.gradle sed -i "s/versionName \"[^\"]*\"/versionName \"${NEW_VERSION_NAME}\"/" app/build.gradle - name: Print Version run: | echo "Version Name: $NEW_VERSION_NAME" echo "Version Code: $NEW_VERSION_CODE" - name: Set up JDK 21 uses: actions/setup-java@v4 with: distribution: "temurin" java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Build with Gradle run: | chmod +x ./gradlew ./gradlew assembleRelease tree app/build/outputs/apk/release - uses: qlenlen/sign-android-release@v2.0.1 name: Sign app APK id: sign_app with: releaseDirectory: app/build/outputs/apk/release signingKeyBase64: ${{ secrets.KEYSTORE }} alias: ${{ secrets.KEY_ALIAS }} keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }} keyPassword: ${{ secrets.KEY_PASSWORD }} env: BUILD_TOOLS_VERSION: "35.0.0" - name: Commit the changes run: | # Configure git user git config user.name "github-actions" git config user.email "github-actions@users.noreply.github.com" # Add modified build.gradle file git add app/build.gradle # Commit the changes git commit -m "Update versionName and versionCode to ${NEW_VERSION_NAME} and ${NEW_VERSION_CODE}" # Push changes to the current branch git push https://github-actions:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} HEAD:${GITHUB_REF#refs/heads/} - name: Rename APK run: | ls -al app/build/outputs/apk/release echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}" cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher_${{ env.NEW_VERSION_NAME }}.apk - name: Upload APK uses: actions/upload-artifact@v4.3.5 with: name: KernelFlasher_${{ env.NEW_VERSION_NAME }} path: KernelFlasher_${{ env.NEW_VERSION_NAME }}.apk - name: Upload release uses: ncipollo/release-action@v1.14.0 with: allowUpdates: true removeArtifacts: true draft: true name: ${{ env.NEW_VERSION_NAME }} tag: "v${{ env.NEW_VERSION_NAME }}" body: | Note: KernelFlasher + allow-errors Changes in this Release: ${{ github.event.inputs.changes_in_release }} artifacts: "*.apk" ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea .DS_Store /app/release /build /captures .externalNativeBuild .cxx local.properties .secrets .env .github/workflows/build_local.yml KernelFlasher.apk /app/build/* ================================================ FILE: LICENSE ================================================ Copyright (c) 2022 capntrips Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. This project bundles lptools (https://github.com/phhusson/vendor_lptools), which is licensed under the Apache 2.0 license: Copyright (C) 2020 Pierre-Hugues Husson Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. This project bundles magiskboot (https://github.com/topjohnwu/Magisk), which is licensed under the GPLv3+ license: Copyright (C) 2017-2022 John Wu <@topjohnwu> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ================================================ FILE: README.md ================================================ [![GitHub release](https://img.shields.io/github/release/fatalcoder524/KernelFlasher)](https://GitHub.com/fatalcoder524/KernelFlasher/releases/) [![Github all releases](https://img.shields.io/github/downloads/fatalcoder524/KernelFlasher/total)](https://GitHub.com/fatalcoder524/KernelFlasher/releases/) # Kernel Flasher Kernel Flasher is an Android app to flash, backup, and restore kernels. ## Usage `View` a slot and choose to `Flash` an AK3 zip, `Backup` the kernel related partitions, or `Restore` a previous backup. There are also options to toggle the mount and map status of `vendor_dlkm` and to save `dmesg` and `logcat`. ================================================ FILE: app/.gitignore ================================================ /build /release build.gradle.bak *.bak ================================================ FILE: app/build.gradle.kts ================================================ plugins { alias(libs.plugins.android.application) alias(libs.plugins.devtools.ksp) alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.kotlin.compose.compiler) } android { compileSdk = 36 namespace = "com.github.capntrips.kernelflasher" defaultConfig { applicationId = "com.github.capntrips.kernelflasher" minSdk = 29 targetSdk = 36 versionCode = 10600 versionName = "1.6.0" javaCompileOptions { annotationProcessorOptions { arguments += mapOf( "room.schemaLocation" to "$projectDir/schemas", "room.incremental" to "true", ) } } ndk { //noinspection ChromeOsAbiSupport abiFilters.add("arm64-v8a") } vectorDrawables { useSupportLibrary = true } } buildTypes { release { isMinifyEnabled = false isShrinkResources = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } sourceSets { getByName("main") { jniLibs.srcDirs("src/main/jniLibs") } } buildFeatures { buildConfig = true aidl = true compose = true } compileOptions { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 } kotlin { jvmToolchain(21) } packaging { resources { excludes += setOf("/META-INF/{AL2.0,LGPL2.1}") } jniLibs { useLegacyPackaging = true } dex { useLegacyPackaging = true } } androidResources { generateLocaleConfig = true } ksp { arg("room.schemaLocation", "$projectDir/schemas") arg("room.incremental", "true") } } dependencies { implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.ui) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.navigation.compose) implementation(libs.androidx.room.runtime) annotationProcessor(libs.androidx.room.compiler) ksp(libs.androidx.room.compiler) implementation(libs.libsu.core) implementation(libs.libsu.io) implementation(libs.libsu.nio) implementation(libs.libsu.service) implementation(libs.material) implementation(libs.okhttp) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json) implementation(libs.retrofit) implementation(libs.converter.gson) } ================================================ FILE: app/proguard-rules.pro ================================================ # GENERAL -dontobfuscate -keepattributes Signature -keepattributes RuntimeVisibleAnnotations # ANDROID INTERFACES / AIDL -keep class com.github.capntrips.kernelflasher.FilesystemService -keep class * implements android.os.IInterface -keepclassmembers class * { public static ** asInterface(android.os.IBinder); } # Prevent R8 from stripping native methods used via JNI -keepclasseswithmembernames class * { native ; } # Keep RootShell library (libsu) -keep class com.topjohnwu.superuser.** { *; } # RETROFIT2 -keep interface com.github.capntrips.kernelflasher.GitHubApi { *; } -keep class retrofit2.** { *; } -dontwarn retrofit2.** # ============ GSON ============ -keep class com.google.gson.** { *; } -keep class com.github.capntrips.kernelflasher.AppUpdater$* { *; } # Keep all fields/methods annotated with Gson's @SerializedName -keepclassmembers class * { @com.google.gson.annotations.SerializedName ; } # ============ KOTLIN ============ -keep class kotlin.Metadata { *; } -keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations,Exceptions,InnerClasses,EnclosingMethod,Signature,SourceFile,LineNumberTable,*Annotation*,Deprecated,SourceDir,CompilationID,LocalVariableTable,LocalVariableTypeTable,Module* -keepclassmembers class **.AppUpdater$GitHubRelease { ; } -keepclassmembers class **.AppUpdater$GitHubAsset { ; } # ============ COROUTINES ============ -keepclassmembers class kotlinx.coroutines.BuildConfig { public static final boolean DEBUG; } -keep class kotlinx.** { *; } -dontwarn kotlinx.** # ============ DOWNLOAD MANAGER & BROADCAST RECEIVER ============ -keep class com.github.capntrips.kernelflasher.AppUpdater { *; } -keepclassmembers class com.github.capntrips.kernelflasher.AppUpdater { *; } # Keep VectorDrawableCompat to avoid crashes or inflation errors -keep class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat { *; } ================================================ FILE: app/schemas/com.github.capntrips.kernelflasher.common.types.room.AppDatabase/1.json ================================================ { "formatVersion": 1, "database": { "version": 1, "identityHash": "bbe3033de836fa33fb2ed46b5272124e", "entities": [ { "tableName": "Update", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `update_uri` TEXT, `kernel_name` TEXT NOT NULL, `kernel_version` TEXT NOT NULL, `kernel_link` TEXT NOT NULL, `kernel_changelog_url` TEXT NOT NULL, `kernel_date` INTEGER NOT NULL, `kernel_sha1` TEXT NOT NULL, `support_link` TEXT, `last_updated` INTEGER, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", "columnName": "id", "affinity": "INTEGER" }, { "fieldPath": "updateUri", "columnName": "update_uri", "affinity": "TEXT" }, { "fieldPath": "kernelName", "columnName": "kernel_name", "affinity": "TEXT", "notNull": true }, { "fieldPath": "kernelVersion", "columnName": "kernel_version", "affinity": "TEXT", "notNull": true }, { "fieldPath": "kernelLink", "columnName": "kernel_link", "affinity": "TEXT", "notNull": true }, { "fieldPath": "kernelChangelogUrl", "columnName": "kernel_changelog_url", "affinity": "TEXT", "notNull": true }, { "fieldPath": "kernelDate", "columnName": "kernel_date", "affinity": "INTEGER", "notNull": true }, { "fieldPath": "kernelSha1", "columnName": "kernel_sha1", "affinity": "TEXT", "notNull": true }, { "fieldPath": "supportLink", "columnName": "support_link", "affinity": "TEXT" }, { "fieldPath": "lastUpdated", "columnName": "last_updated", "affinity": "INTEGER" } ], "primaryKey": { "autoGenerate": false, "columnNames": [ "id" ] } } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbe3033de836fa33fb2ed46b5272124e')" ] } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl ================================================ package com.github.capntrips.kernelflasher; interface IFilesystemService { IBinder getFileSystemService(); } ================================================ FILE: app/src/main/assets/flash_ak3.sh ================================================ #!/system/bin/sh ## setup for testing: unzip -p "$Z" tools*/busybox > $F/busybox_ak; unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary; ## chmod 755 $F/busybox_ak; $F/busybox_ak >/dev/null 2>&1 if [ $? -eq 0 ]; then mv $F/busybox $F/busybox_orig mv $F/busybox_ak $F/busybox fi $F/busybox chmod 755 $F/update-binary; $F/busybox chown root:root $F/busybox $F/update-binary; TMP=$F/tmp; $F/busybox umount $TMP 2>/dev/null; $F/busybox rm -rf $TMP 2>/dev/null; $F/busybox mkdir -p $TMP; $F/busybox mount -t tmpfs -o noatime tmpfs $TMP; #$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1; PATTERN='\$[Bb][Bb] chmod -R 755 tools bin;'; sed -i "/$PATTERN/i cp -f \"\$F/busybox\" \$AKHOME/tools;" "$F/update-binary"; # update-binary AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z"; RC=$?; $F/busybox umount $TMP; $F/busybox rm -rf $TMP; $F/busybox mount -o ro,remount -t auto /; $F/busybox rm -f $F/update-binary $F/busybox; mv $F/busybox_orig $F/busybox # work around libsu not cleanly accepting return or exit as last line safereturn() { return $RC; } safereturn; ================================================ FILE: app/src/main/assets/flash_ak3_mkbootfs.sh ================================================ #!/system/bin/sh ## setup for testing: unzip -p "$Z" tools*/busybox > $F/busybox_ak; unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary; ## chmod 755 $F/busybox_ak; $F/busybox_ak >/dev/null 2>&1 if [ $? -eq 0 ]; then mv $F/busybox $F/busybox_orig mv $F/busybox_ak $F/busybox fi $F/busybox chmod 755 $F/update-binary; $F/busybox chown root:root $F/busybox $F/update-binary; TMP=$F/tmp; $F/busybox umount $TMP 2>/dev/null; $F/busybox rm -rf $TMP 2>/dev/null; $F/busybox mkdir -p $TMP; $F/busybox mount -t tmpfs -o noatime tmpfs $TMP; #$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1; PATTERN='\$[Bb][Bb] chmod -R 755 tools bin;'; sed -i "/$PATTERN/i cp -f \"\$F/mkbootfs\" \$AKHOME/tools;" "$F/update-binary"; sed -i "/$PATTERN/i cp -f \"\$F/busybox\" \$AKHOME/tools;" "$F/update-binary"; # update-binary AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z"; RC=$?; $F/busybox umount $TMP; $F/busybox rm -rf $TMP; $F/busybox mount -o ro,remount -t auto /; $F/busybox rm -f $F/update-binary $F/busybox; mv $F/busybox_orig $F/busybox # work around libsu not cleanly accepting return or exit as last line safereturn() { return $RC; } safereturn; ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/AppUpdater.kt ================================================ package com.github.capntrips.kernelflasher import android.annotation.SuppressLint import android.app.DownloadManager import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.net.Uri import android.os.Environment import android.widget.Toast import com.google.gson.Gson import com.google.gson.annotations.SerializedName import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.GET import java.io.IOException import java.net.HttpURLConnection import java.net.URL import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext interface GitHubApi { @GET("repos/fatalcoder524/KernelFlasher/releases/latest") suspend fun getLatestRelease(): Response } object AppUpdater { data class GitHubAsset( val name: String, @SerializedName("browser_download_url") val downloadUrl: String ) data class GitHubRelease( @SerializedName("tag_name") val tagName: String, val body: String, val assets: List ) private val api: GitHubApi = Retrofit.Builder() .baseUrl("https://api.github.com/") .addConverterFactory(GsonConverterFactory.create(Gson())) .build() .create(GitHubApi::class.java) // Compares version strings (e.g., v1.0.0 vs. v1.0.1) private fun isNewer(latest: String, current: String): Boolean { val latestParts = latest.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 } val currentParts = current.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 } return latestParts.zip(currentParts).any { (l, c) -> l > c } } suspend fun hasActiveInternetConnection(): Boolean = withContext(Dispatchers.IO) { try { val url = URL("https://connectivitycheck.gstatic.com/generate_204") val connection = url.openConnection() as HttpURLConnection connection.setRequestProperty("User-Agent", "Android") connection.connectTimeout = 1500 connection.connect() return@withContext connection.responseCode == 204 } catch (e: IOException) { return@withContext false } } // Checks if an update is available suspend fun checkForUpdate( context: Context, currentVersion: String, onShowDialog: (String, List, () -> Unit) -> Unit ) { val response = api.getLatestRelease() if (response.isSuccessful) { val release = response.body() ?: return val latestVersion = release.tagName.removePrefix("v") if (isNewer(latestVersion, currentVersion)) { val apk = release.assets.find { it.name.endsWith(".apk") } ?: return val dialogTitle = "New version: $latestVersion" val dialogLines = listOf( "Changelog:", *release.body.split("\n").toTypedArray() ) val confirmAction = { downloadAndInstallApk(context, apk.downloadUrl, latestVersion) } onShowDialog(dialogTitle, dialogLines, confirmAction) } } } @SuppressLint("UnspecifiedRegisterReceiverFlag") private fun downloadAndInstallApk(context: Context, url: String, latestVersion: String) { Toast.makeText(context, "Downloading Update in Background. Don't perform any operations till update is completed!", Toast.LENGTH_LONG).show() val request = DownloadManager.Request(Uri.parse(url)) request.setTitle("Kernel Flasher Latest Download") request.setDescription("Downloading update...") request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Kernel_Flasher_$latestVersion.apk") request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val id = manager.enqueue(request) val receiver = object : BroadcastReceiver() { override fun onReceive(c: Context?, intent: Intent?) { val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) if (id == downloadId) { val apkUri = manager.getUriForDownloadedFile(id) val installIntent = Intent(Intent.ACTION_VIEW).apply { setDataAndType(apkUri, "application/vnd.android.package-archive") flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION } context.startActivity(installIntent) } } } val appContext = context.applicationContext val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) { appContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED) } else { @Suppress("DEPRECATION") appContext.registerReceiver(receiver, intentFilter) } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt ================================================ package com.github.capntrips.kernelflasher import android.content.Intent import android.os.IBinder import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.nio.FileSystemManager class FilesystemService : RootService() { inner class FilesystemIPC : IFilesystemService.Stub() { override fun getFileSystemService(): IBinder { return FileSystemManager.getService() } } override fun onBind(intent: Intent): IBinder { return FilesystemIPC() } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/MainActivity.kt ================================================ package com.github.capntrips.kernelflasher import android.animation.ObjectAnimator import android.animation.PropertyValuesHolder import android.annotation.SuppressLint import android.app.Activity import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.net.Uri import android.os.Build import android.os.Bundle import android.os.IBinder import android.provider.DocumentsContract import android.util.Log import android.view.View import android.view.ViewTreeObserver import android.view.Window import android.view.animation.AccelerateInterpolator import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.activity.compose.setContent import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.TextButton import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import androidx.core.animation.doOnEnd import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.github.capntrips.kernelflasher.ui.components.DialogButton import com.github.capntrips.kernelflasher.ui.screens.RefreshableScreen import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsContent import com.github.capntrips.kernelflasher.ui.screens.backups.SlotBackupsContent import com.github.capntrips.kernelflasher.ui.screens.error.ErrorScreen import com.github.capntrips.kernelflasher.ui.screens.main.MainContent import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootContent import com.github.capntrips.kernelflasher.ui.screens.slot.SlotContent import com.github.capntrips.kernelflasher.ui.screens.slot.SlotFlashContent import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesAddContent import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesChangelogContent import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesContent import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewContent import com.github.capntrips.kernelflasher.ui.theme.KernelFlasherTheme import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.nio.FileSystemManager import kotlinx.serialization.ExperimentalSerializationApi import java.io.File import kotlin.system.exitProcess object SharedViewModels { @OptIn(ExperimentalSerializationApi::class) lateinit var mainViewModel: MainViewModel } @ExperimentalAnimationApi @ExperimentalMaterialApi @ExperimentalMaterial3Api @ExperimentalSerializationApi @ExperimentalUnitApi class MainActivity : ComponentActivity() { companion object { const val TAG: String = "MainActivity" init { Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)) } } private var rootServiceConnected: Boolean = false private var viewModel: MainViewModel? = null private lateinit var mainListener: MainListener var isAwaitingResult = false inner class AidlConnection : ServiceConnection { override fun onServiceConnected(name: ComponentName, service: IBinder) { if (!rootServiceConnected) { val ipc: IFilesystemService = IFilesystemService.Stub.asInterface(service) val binder: IBinder = ipc.fileSystemService onAidlConnected(FileSystemManager.getRemote(binder)) rootServiceConnected = true } } override fun onServiceDisconnected(name: ComponentName) { setContent { KernelFlasherTheme { ErrorScreen(stringResource(R.string.root_service_disconnected)) } } } } private fun copyAsset(filename: String) { val dest = File(filesDir, filename) assets.open(filename).use { inputStream -> dest.outputStream().use { outputStream -> inputStream.copyTo(outputStream) } } Shell.cmd("chmod +x $dest").exec() } private fun copyNativeBinary(filename: String) { val binary = File(applicationInfo.nativeLibraryDir, "lib$filename.so") println("binary: $binary") val dest = File(filesDir, filename) println("dest: $dest") binary.inputStream().use { inputStream -> dest.outputStream().use { outputStream -> inputStream.copyTo(outputStream) } } Shell.cmd("chmod +x $dest").exec() } override fun onCreate(savedInstanceState: Bundle?) { requestWindowFeature(Window.FEATURE_NO_TITLE) // Hide the title bar val splashScreen = installSplashScreen() super.onCreate(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) val isZipIntent = intent?.action == Intent.ACTION_VIEW && (intent.type == "application/zip" || intent.data?.toString()?.endsWith(".zip") == true) splashScreen.setOnExitAnimationListener { splashScreenView -> val duration = if (isZipIntent) 100L else 250L val scale = ObjectAnimator.ofPropertyValuesHolder( splashScreenView.view, PropertyValuesHolder.ofFloat( View.SCALE_X, 1f, 0f ), PropertyValuesHolder.ofFloat( View.SCALE_Y, 1f, 0f ) ) scale.interpolator = AccelerateInterpolator() scale.duration = duration scale.doOnEnd { splashScreenView.remove() } scale.start() } val content: View = findViewById(android.R.id.content) content.viewTreeObserver.addOnPreDrawListener( object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { return if (viewModel?.isRefreshing == false || Shell.isAppGrantedRoot() == false) { content.viewTreeObserver.removeOnPreDrawListener(this) true } else { false } } } ) Shell.getShell() if (Shell.isAppGrantedRoot()!!) { val intent = Intent(this, FilesystemService::class.java) RootService.bind(intent, AidlConnection()) } else { setContent { KernelFlasherTheme { ErrorScreen(stringResource(R.string.root_required)) } } } } @SuppressLint("WrongConstant") private fun handleZipIntent(intent: Intent?) { val action = intent?.action ?: return val uri = when (action) { Intent.ACTION_VIEW -> intent.data Intent.ACTION_SEND -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) } else { @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_STREAM) } else -> null } ?: return if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) { if(uri.scheme == "content" && DocumentsContract.isDocumentUri(this, uri)) { val takeFlags = intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) try { contentResolver.takePersistableUriPermission(uri, takeFlags) } catch (se: SecurityException) { Log.e(MainViewModel.Companion.TAG, se.message, se) } } viewModel?.pendingFlashUri = uri if(viewModel?.isAb == true) viewModel?.showSlotIntentDialog?.value = true else { viewModel?.slotSuffixForFlash?.value = null viewModel?.slotSuffixForFlash?.value = viewModel?.slotSuffix } } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) if (Shell.isAppGrantedRoot() == true) { handleZipIntent(intent) if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) { intent.replaceExtras(Bundle()) // Clear any existing data setIntent(Intent()) // Replace with empty intent } } } fun onAidlConnected(fileSystemManager: FileSystemManager) { try { Shell.cmd("cd $filesDir").exec() copyNativeBinary("lptools_static") // v20220825 copyNativeBinary("httools_static") // v3.2.0 copyNativeBinary("magiskboot") // v29.0 copyNativeBinary("bootctl") // aosp_arm64-img-13613025 android14 copyNativeBinary("busybox") // BusyBox v1.36.1.1 copyAsset("mkbootfs") copyAsset("ksuinit") copyAsset("flash_ak3.sh") copyAsset("flash_ak3_mkbootfs.sh") } catch (e: Exception) { Log.e(TAG, e.message, e) setContent { KernelFlasherTheme { ErrorScreen(e.message!!) } } } setContent { val navController = rememberNavController() viewModel = viewModel { val application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)) MainViewModel(application, fileSystemManager, navController) } val mainViewModel = viewModel!! SharedViewModels.mainViewModel = mainViewModel val slotSuffix by viewModel!!.slotSuffixForFlash handleZipIntent(intent) if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) { intent.replaceExtras(Bundle()) // Clear any existing data setIntent(Intent()) // Replace with empty intent } val context = LocalContext.current val dialogData = viewModel!!.updateDialogData LaunchedEffect(Unit) { if(AppUpdater.hasActiveInternetConnection()) { AppUpdater.checkForUpdate( context.applicationContext, BuildConfig.VERSION_NAME ) { title, lines, confirm -> viewModel!!.showUpdateDialog(title, lines, confirm) } } val uri = viewModel?.pendingFlashUri if (uri != null) { if (viewModel?.isAb == true && slotSuffix == null) { viewModel?.pendingFlashUri = uri viewModel?.showSlotIntentDialog?.value = true } else { // Already have slot or not AB - flash directly if (viewModel?.isAb == true && slotSuffix == "_b") { viewModel?.slotB?.flashActionType = "flashAk3" viewModel?.slotB?.flashActionURI = uri viewModel?.slotB?.showConfirmDialog() } else { viewModel?.slotA?.flashActionType = "flashAk3" viewModel?.slotA?.flashActionURI = uri viewModel?.slotA?.showConfirmDialog() } navController.navigate("slot${slotSuffix}") navController.navigate("slot${slotSuffix}/flash") { popUpTo("slot${slotSuffix}") } viewModel?.pendingFlashUri = null viewModel?.slotSuffixForFlash?.value = null } } } var showExitDialog by remember { mutableStateOf(false) } KernelFlasherTheme { if (!mainViewModel.hasError) { mainListener = MainListener { mainViewModel.refresh(this) } val slotViewModelA = mainViewModel.slotA val slotViewModelB = mainViewModel.slotB val backupsViewModel = mainViewModel.backups val updatesViewModel = mainViewModel.updates val rebootViewModel = mainViewModel.reboot BackHandler(enabled = !mainViewModel.isRefreshing, onBack = {}) // New back handler for exit BackHandler(enabled = true) { showExitDialog = true } val slotContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_a" val slotViewModel = slotViewModelA if (slotViewModel.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) { slotViewModel.clearFlash(this@MainActivity) } RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { SlotContent(slotViewModel, slotSuffix, navController) } } val slotContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_b" val slotViewModel = slotViewModelB if (slotViewModel!!.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) { slotViewModel.clearFlash(this@MainActivity) } RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { SlotContent(slotViewModel, slotSuffix, navController) } } val slotContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "" val slotViewModel = slotViewModelA if (slotViewModel.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) { slotViewModel.clearFlash(this@MainActivity) } RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { SlotContent(slotViewModel, slotSuffix, navController) } } val slotFlashContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_a" val slotViewModel = slotViewModelA RefreshableScreen(mainViewModel, navController) { SlotFlashContent(slotViewModel, slotSuffix, navController) } } val slotFlashContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_b" val slotViewModel = slotViewModelB RefreshableScreen(mainViewModel, navController) { SlotFlashContent(slotViewModel!!, slotSuffix, navController) } } val slotFlashContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "" val slotViewModel = slotViewModelA RefreshableScreen(mainViewModel, navController) { SlotFlashContent(slotViewModel, slotSuffix, navController) } } val slotBackupsContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_a" val slotViewModel = slotViewModelA if (backStackEntry.arguments?.getString("backupId") != null) { backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") } else { backupsViewModel.clearCurrent() } RefreshableScreen(mainViewModel, navController) { SlotBackupsContent(slotViewModel, backupsViewModel, slotSuffix, navController) } } val slotBackupsContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_b" val slotViewModel = slotViewModelB if (backStackEntry.arguments?.getString("backupId") != null) { backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") } else { backupsViewModel.clearCurrent() } RefreshableScreen(mainViewModel, navController) { SlotBackupsContent(slotViewModel!!, backupsViewModel, slotSuffix, navController) } } val slotBackupsContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "" val slotViewModel = slotViewModelA if (backStackEntry.arguments?.getString("backupId") != null) { backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") } else { backupsViewModel.clearCurrent() } RefreshableScreen(mainViewModel, navController) { SlotBackupsContent(slotViewModel, backupsViewModel, slotSuffix, navController) } } val slotBackupFlashContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_a" val slotViewModel = slotViewModelA backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { RefreshableScreen(mainViewModel, navController) { SlotFlashContent(slotViewModel, slotSuffix, navController) } } } val slotBackupFlashContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "_b" val slotViewModel = slotViewModelB backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { RefreshableScreen(mainViewModel, navController) { SlotFlashContent(slotViewModel!!, slotSuffix, navController) } } } val slotBackupFlashContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> val slotSuffix = "" val slotViewModel = slotViewModelA backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { RefreshableScreen(mainViewModel, navController) { SlotFlashContent(slotViewModel, slotSuffix, navController) } } } NavHost(navController = navController, startDestination = "main") { composable("main") { RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { MainContent(mainViewModel, navController) } } if (mainViewModel.isAb) { composable("slot_a", content = slotContentA) composable("slot_a/flash", content = slotFlashContentA) composable("slot_a/flash/ak3", content = slotFlashContentA) composable("slot_a/flash/image", content = slotFlashContentA) composable("slot_a/flash/image/flash", content = slotFlashContentA) composable("slot_a/backup", content = slotFlashContentA) composable("slot_a/backup/backup", content = slotFlashContentA) composable("slot_a/backups", content = slotBackupsContentA) composable("slot_a/backups/{backupId}", content = slotBackupsContentA) composable("slot_a/backups/{backupId}/restore", content = slotBackupsContentA) composable("slot_a/backups/{backupId}/restore/restore", content = slotBackupsContentA) composable("slot_a/backups/{backupId}/flash/ak3", content = slotBackupFlashContentA) composable("slot_b", content = slotContentB) composable("slot_b/flash", content = slotFlashContentB) composable("slot_b/flash/ak3", content = slotFlashContentB) composable("slot_b/flash/image", content = slotFlashContentB) composable("slot_b/flash/image/flash", content = slotFlashContentB) composable("slot_b/backup", content = slotFlashContentB) composable("slot_b/backup/backup", content = slotFlashContentB) composable("slot_b/backups", content = slotBackupsContentB) composable("slot_b/backups/{backupId}", content = slotBackupsContentB) composable("slot_b/backups/{backupId}/restore", content = slotBackupsContentB) composable("slot_b/backups/{backupId}/restore/restore", content = slotBackupsContentB) composable("slot_b/backups/{backupId}/flash/ak3", content = slotBackupFlashContentB) } else { composable("slot", content = slotContent) composable("slot/flash", content = slotFlashContent) composable("slot/flash/ak3", content = slotFlashContent) composable("slot/flash/image", content = slotFlashContent) composable("slot/flash/image/flash", content = slotFlashContent) composable("slot/backup", content = slotFlashContent) composable("slot/backup/backup", content = slotFlashContent) composable("slot/backups", content = slotBackupsContent) composable("slot/backups/{backupId}", content = slotBackupsContent) composable("slot/backups/{backupId}/restore", content = slotBackupsContent) composable("slot/backups/{backupId}/restore/restore", content = slotBackupsContent) composable("slot/backups/{backupId}/flash/ak3", content = slotBackupFlashContent) } composable("backups") { backupsViewModel.clearCurrent() RefreshableScreen(mainViewModel, navController) { BackupsContent(backupsViewModel, navController) } } composable("backups/{backupId}") { backStackEntry -> backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { RefreshableScreen(mainViewModel, navController) { BackupsContent(backupsViewModel, navController) } } } composable("updates") { updatesViewModel.clearCurrent() RefreshableScreen(mainViewModel, navController) { UpdatesContent(updatesViewModel, navController) } } composable("updates/add") { RefreshableScreen(mainViewModel, navController) { UpdatesAddContent(updatesViewModel, navController) } } composable("updates/view/{updateId}") { backStackEntry -> val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt() val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId } updatesViewModel.currentUpdate = currentUpdate if (updatesViewModel.currentUpdate != null) { // TODO: enable swipe refresh RefreshableScreen(mainViewModel, navController) { UpdatesViewContent(updatesViewModel, navController) } } } composable("updates/view/{updateId}/changelog") { backStackEntry -> val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt() val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId } updatesViewModel.currentUpdate = currentUpdate if (updatesViewModel.currentUpdate != null) { RefreshableScreen(mainViewModel, navController) { UpdatesChangelogContent(updatesViewModel, navController) } } } composable("reboot") { RefreshableScreen(mainViewModel, navController) { RebootContent(rebootViewModel, navController) } } composable("error/{error}") { backStackEntry -> val error = backStackEntry.arguments?.getString("error") ErrorScreen(error!!) } } } else { ErrorScreen(mainViewModel.error) } if (dialogData != null) { AlertDialog( onDismissRequest = { viewModel!!.hideUpdateDialog() }, title = { Text( dialogData.title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold ) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { dialogData.changelog.forEach { Text(it, fontWeight = FontWeight.Bold) } } }, confirmButton = { DialogButton("Update APK") { viewModel!!.hideUpdateDialog() dialogData.onConfirm() } }, dismissButton = { DialogButton("CANCEL") { viewModel!!.hideUpdateDialog() } }, modifier = Modifier.padding(16.dp) ) } if (showExitDialog) { AlertDialog( onDismissRequest = { showExitDialog = false }, title = { Text("Exit App") }, text = { Text("Are you sure you want to exit?") }, confirmButton = { TextButton(onClick = { (context as? Activity)?.let { it.finishAffinity() exitProcess(0) } }) { Text("Yes") } }, dismissButton = { TextButton(onClick = { showExitDialog = false }) { Text("No") } } ) } if (viewModel?.showSlotIntentDialog?.value == true) { AlertDialog( onDismissRequest = { viewModel?.showSlotIntentDialog?.value = false }, title = { Text("Select Slot to Flash") }, text = { Text("Choose the slot where the zip should be flashed.") }, confirmButton = { TextButton(onClick = { viewModel?.slotSuffixForFlash?.value = null viewModel?.slotSuffixForFlash?.value = if(viewModel?.slotSuffix == "_a") "_b" else "_a" viewModel?.showSlotIntentDialog?.value = false }) { Text("Inactive Slot") } }, dismissButton = { TextButton(onClick = { viewModel?.slotSuffixForFlash?.value = null viewModel?.slotSuffixForFlash?.value = viewModel?.slotSuffix viewModel?.showSlotIntentDialog?.value = false }) { Text("Active Slot") } } ) } LaunchedEffect(slotSuffix) { val uri = viewModel!!.pendingFlashUri if (uri != null && slotSuffix != null) { if (viewModel?.isAb == true && slotSuffix == "_b") { viewModel?.slotB?.flashActionType = "flashAk3" viewModel?.slotB?.flashActionURI = uri viewModel?.slotB?.showConfirmDialog() } else { viewModel?.slotA?.flashActionType = "flashAk3" viewModel?.slotA?.flashActionURI = uri viewModel?.slotA?.showConfirmDialog() } navController.navigate("slot${slotSuffix}") navController.navigate("slot${slotSuffix}/flash") { popUpTo("slot${slotSuffix}") } viewModel!!.pendingFlashUri = null viewModel!!.slotSuffixForFlash.value = null } } } } } public override fun onResume() { super.onResume() if (this::mainListener.isInitialized) { if (!isAwaitingResult) { mainListener.resume() } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt ================================================ package com.github.capntrips.kernelflasher internal class MainListener(private val callback: () -> Unit) { fun resume() { callback.invoke() } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt ================================================ package com.github.capntrips.kernelflasher.common import android.content.Context import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex import com.github.capntrips.kernelflasher.common.types.partitions.FstabEntry import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.ExtendedFile import com.topjohnwu.superuser.nio.FileSystemManager import kotlinx.serialization.json.Json import java.io.File import java.security.DigestOutputStream import java.security.MessageDigest object PartitionUtil { val PartitionNames = listOf( "boot", "dtbo", "init_boot", "recovery", "system_dlkm", "vbmeta", "vendor_boot", "vendor_dlkm", "vendor_kernel_boot" ) val AvailablePartitions = mutableListOf() private var fileSystemManager: FileSystemManager? = null private var bootParent: File? = null fun init(context: Context, fileSystemManager: FileSystemManager) { this.fileSystemManager = fileSystemManager val fstabEntry = findPartitionFstabEntry(context, "boot") if (fstabEntry != null) { bootParent = File(fstabEntry.blkDevice).parentFile } val activeSlotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] for (partitionName in PartitionNames) { val blockDevice = findPartitionBlockDevice(context, partitionName, activeSlotSuffix) if (blockDevice != null && blockDevice.exists()) { AvailablePartitions.add(partitionName) } } } private fun findPartitionFstabEntry(context: Context, partitionName: String): FstabEntry? { val httools = File(context.filesDir, "httools_static") val result = Shell.cmd("$httools dump $partitionName").exec().out if (result.isNotEmpty() && result[0].trim().startsWith("{")) { return Json.decodeFromString(result[0]) } return null } fun isPartitionLogical(context: Context, partitionName: String): Boolean { return findPartitionFstabEntry(context, partitionName)?.fsMgrFlags?.logical == true } fun findPartitionBlockDevice(context: Context, partitionName: String, slotSuffix: String): ExtendedFile? { var blockDevice: ExtendedFile? = null val fstabEntry = findPartitionFstabEntry(context, partitionName) if (fstabEntry != null) { if (fstabEntry.fsMgrFlags?.logical == true) { if (fstabEntry.logicalPartitionName == "$partitionName$slotSuffix") { blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) } } else { blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) if (blockDevice.name != "$partitionName$slotSuffix") { blockDevice = fileSystemManager!!.getFile(blockDevice.parentFile, "$partitionName$slotSuffix") } } } if (blockDevice == null || !blockDevice.exists()) { val siblingDevice = if (bootParent != null) fileSystemManager!!.getFile(bootParent!!, "$partitionName$slotSuffix") else null val physicalDevice = fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix") val logicalDevice = fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix") if (siblingDevice?.exists() == true) { blockDevice = physicalDevice } else if (physicalDevice.exists()) { blockDevice = physicalDevice } else if (logicalDevice.exists()) { blockDevice = logicalDevice } } return blockDevice } @Suppress("unused") fun partitionAvb(context: Context, partitionName: String): String { val httools = File(context.filesDir, "httools_static") val result = Shell.cmd("$httools avb $partitionName").exec().out return if (result.isNotEmpty()) result[0] else "" } fun flashBlockDevice(image: ExtendedFile, blockDevice: ExtendedFile, hashAlgorithm: String): String { val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt() val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() if (partitionSize < imageSize) { throw Error("Partition ${blockDevice.name} is smaller than image") } if (partitionSize > imageSize) { Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice && sync").exec() } val messageDigest = MessageDigest.getInstance(hashAlgorithm) image.newInputStream().use { inputStream -> blockDevice.newOutputStream().use { outputStream -> DigestOutputStream(outputStream, messageDigest).use { digestOutputStream -> inputStream.copyTo(digestOutputStream) } } } return messageDigest.digest().toHex() } @Suppress("SameParameterValue") fun flashLogicalPartition(context: Context, image: ExtendedFile, blockDevice: ExtendedFile, partitionName: String, slotSuffix: String, hashAlgorithm: String, addMessage: (message: String) -> Unit): String { val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() val lptools = File(context.filesDir, "lptools_static") Shell.cmd("$lptools remove ${partitionName}_kf").exec() if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) { if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) { if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) { val temporaryBlockDevice = fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf") val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm) if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix").exec().isSuccess) { return hash } else { throw Error("Replacing $partitionName$slotSuffix failed") } } else { throw Error("Remapping ${partitionName}_kf failed") } } else { throw Error("Unmapping ${partitionName}_kf failed") } } else { addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...") val httools = File(context.filesDir, "httools_static") if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) { val verityBlockDevice = blockDevice.parentFile!!.getChildFile("${partitionName}-verity") if (verityBlockDevice.exists()) { if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) { throw Error("Unmapping ${partitionName}-verity failed") } } if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) { if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)").exec().isSuccess) { if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) { val hash = flashBlockDevice(image, blockDevice, hashAlgorithm) if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) { return hash } else { throw Error("Mounting $partitionName failed") } } else { throw Error("Remapping $partitionName$slotSuffix failed") } } else { throw Error("Resizing $partitionName$slotSuffix failed") } } else { throw Error("Unmapping $partitionName$slotSuffix failed") } } else { throw Error("Unmounting $partitionName failed") } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt ================================================ package com.github.capntrips.kernelflasher.common.extensions import kotlin.ByteArray object ByteArray { fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt ================================================ package com.github.capntrips.kernelflasher.common.extensions import com.topjohnwu.superuser.nio.ExtendedFile import java.io.InputStream import java.io.InputStreamReader import java.io.OutputStream import java.nio.charset.Charset object ExtendedFile { private fun ExtendedFile.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = inputStream().reader(charset) private fun ExtendedFile.writeBytes(array: kotlin.ByteArray): Unit = outputStream().use { it.write(array) } fun ExtendedFile.readText(charset: Charset = Charsets.UTF_8): String = reader(charset).use { it.readText() } @Suppress("unused") fun ExtendedFile.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset)) fun ExtendedFile.inputStream(): InputStream = newInputStream() fun ExtendedFile.outputStream(): OutputStream = newOutputStream() } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt ================================================ package com.github.capntrips.kernelflasher.common.types.backups import com.github.capntrips.kernelflasher.common.types.partitions.Partitions import kotlinx.serialization.Serializable @Serializable data class Backup( val name: String, val type: String, val kernelVersion: String, val bootSha1: String? = null, val filename: String? = null, val hashes: Partitions? = null, val hashAlgorithm: String? = null ) ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt ================================================ package com.github.capntrips.kernelflasher.common.types.partitions import kotlinx.serialization.Serializable @Serializable data class FsMgrFlags( val logical: Boolean = false ) ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt ================================================ package com.github.capntrips.kernelflasher.common.types.partitions import kotlinx.serialization.Serializable @Serializable data class FstabEntry( val blkDevice: String, val mountPoint: String, val fsType: String, val logicalPartitionName: String? = null, val avb: Boolean = false, val vbmetaPartition: String? = null, val avbKeys: String? = null, val fsMgrFlags: FsMgrFlags? = null ) ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt ================================================ package com.github.capntrips.kernelflasher.common.types.partitions import kotlinx.serialization.Serializable @Serializable data class Partitions( val boot: String? = null, val dtbo: String? = null, @Suppress("PropertyName") val init_boot: String? = null, val recovery: String? = null, @Suppress("PropertyName") val system_dlkm: String? = null, val vbmeta: String? = null, @Suppress("PropertyName") val vendor_boot: String? = null, @Suppress("PropertyName") val vendor_dlkm: String? = null, @Suppress("PropertyName") val vendor_kernel_boot: String? = null ) { companion object { fun from(sparseMap: Map) = object { val map = sparseMap.withDefault { null } val boot by map val dtbo by map val init_boot by map val recovery by map val system_dlkm by map val vbmeta by map val vendor_boot by map val vendor_dlkm by map val vendor_kernel_boot by map val partitions = Partitions(boot, dtbo, init_boot, recovery, system_dlkm, vbmeta, vendor_boot, vendor_dlkm, vendor_kernel_boot) }.partitions } operator fun get(partition: String): String? { return when (partition) { "boot" -> boot "dtbo" -> dtbo "init_boot" -> init_boot "recovery" -> recovery "system_dlkm" -> system_dlkm "vbmeta" -> vbmeta "vendor_boot" -> vendor_boot "vendor_dlkm" -> vendor_dlkm "vendor_kernel_boot" -> vendor_kernel_boot else -> null } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt ================================================ package com.github.capntrips.kernelflasher.common.types.room import androidx.room.Database import androidx.room.RoomDatabase import androidx.room.TypeConverters import com.github.capntrips.kernelflasher.common.types.room.updates.Update import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateDao @Database(entities = [Update::class], version = 1) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun updateDao(): UpdateDao } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt ================================================ package com.github.capntrips.kernelflasher.common.types.room import androidx.room.TypeConverter import java.util.Date class Converters { @TypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } } @TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt ================================================ package com.github.capntrips.kernelflasher.common.types.room.updates import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonTransformingSerializer import kotlinx.serialization.json.buildJsonObject import java.text.SimpleDateFormat import java.util.Date import java.util.Locale object DateSerializer : KSerializer { override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(formatter.format(value)) override fun deserialize(decoder: Decoder): Date = formatter.parse(decoder.decodeString())!! } object UpdateSerializer : JsonTransformingSerializer(Update.serializer()) { override fun transformSerialize(element: JsonElement): JsonElement { require(element is JsonObject) return buildJsonObject { put("kernel", buildJsonObject { put("name", element["kernelName"]!!) put("version", element["kernelVersion"]!!) put("link", element["kernelLink"]!!) put("changelog_url", element["kernelChangelogUrl"]!!) put("date", element["kernelDate"]!!) put("sha1", element["kernelSha1"]!!) }) if (element["supportLink"] != null) { put("support", buildJsonObject { put("link", element["supportLink"]!!) }) } } } override fun transformDeserialize(element: JsonElement): JsonElement { require(element is JsonObject) val kernel = element["kernel"] val support = element["support"] require(kernel is JsonObject) require(support is JsonObject?) return buildJsonObject { put("kernelName", kernel["name"]!!) put("kernelVersion", kernel["version"]!!) put("kernelLink", kernel["link"]!!) put("kernelChangelogUrl", kernel["changelog_url"]!!) put("kernelDate", kernel["date"]!!) put("kernelSha1", kernel["sha1"]!!) if (support != null && support["link"] != null) { put("supportLink", support["link"]!!) } } } } @Entity @Serializable data class Update( @PrimaryKey @Transient val id: Int? = null, @ColumnInfo(name = "update_uri") @Transient var updateUri: String? = null, @ColumnInfo(name = "kernel_name") var kernelName: String, @ColumnInfo(name = "kernel_version") var kernelVersion: String, @ColumnInfo(name = "kernel_link") var kernelLink: String, @ColumnInfo(name = "kernel_changelog_url") var kernelChangelogUrl: String, @ColumnInfo(name = "kernel_date") @Serializable(DateSerializer::class) var kernelDate: Date, @ColumnInfo(name = "kernel_sha1") var kernelSha1: String, @ColumnInfo(name = "support_link") var supportLink: String?, @ColumnInfo(name = "last_updated") @Transient var lastUpdated: Date? = null, ) ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt ================================================ package com.github.capntrips.kernelflasher.common.types.room.updates import androidx.room.Dao import androidx.room.Delete import androidx.room.Insert import androidx.room.Query @Dao interface UpdateDao { @Query("""SELECT * FROM "update"""") fun getAll(): List @Query("""SELECT * FROM "update" WHERE id IN (:id)""") fun load(id: Int): Update @Insert fun insert(update: Update): Long @androidx.room.Update fun update(update: Update) @Delete fun delete(update: Update) } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp // TODO: Remove when card is supported in material3: https://m3.material.io/components/cards/implementation/android @Composable fun Card( shape: Shape = RoundedCornerShape(4.dp), backgroundColor: Color = MaterialTheme.colorScheme.surface, contentColor: Color = MaterialTheme.colorScheme.onSurface, border: BorderStroke? = null, tonalElevation: Dp = 2.dp, shadowElevation: Dp = 1.dp, content: @Composable ColumnScope.() -> Unit ) { Surface( shape = shape, color = backgroundColor, contentColor = contentColor, tonalElevation = tonalElevation, shadowElevation = shadowElevation, border = border ) { Column( modifier = Modifier .fillMaxWidth() .padding(18.dp, (13.788).dp, 18.dp, 18.dp), content = content ) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @Composable fun DataCard( title: String, button: @Composable (() -> Unit)? = null, content: @Composable (ColumnScope.() -> Unit)? = null ) { Card { Row( modifier = Modifier .fillMaxWidth() .padding(0.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier.padding(0.dp, 9.dp, 8.dp, 9.dp).weight(1.0f), text = title, color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleLarge ) if (button != null) { button() } } if (content != null) { Spacer(Modifier.height(10.dp)) content() } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.layout import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp @Composable fun DataRow( label: String, value: String, labelColor: Color = Color.Unspecified, labelStyle: TextStyle = MaterialTheme.typography.labelMedium, valueColor: Color = Color.Unspecified, valueStyle: TextStyle = MaterialTheme.typography.titleSmall, mutableMaxWidth: MutableState? = null, clickable: Boolean = false, ) { Row { val modifier = if (mutableMaxWidth != null) { var maxWidth by mutableMaxWidth Modifier .layout { measurable, constraints -> val placeable = measurable.measure(constraints) maxWidth = maxOf(maxWidth, placeable.width) layout(width = maxWidth, height = placeable.height) { placeable.placeRelative(0, 0) } } .alignByBaseline() } else { Modifier .alignByBaseline() } Text( modifier = modifier, text = label, color = labelColor, style = labelStyle ) Spacer(Modifier.width(8.dp)) DataValue(value, valueColor, valueStyle, clickable) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp @Composable fun DataSet( label: String, labelColor: Color = Color.Unspecified, labelStyle: TextStyle = MaterialTheme.typography.labelMedium, content: @Composable (ColumnScope.() -> Unit) ) { Text( text = label, color = labelColor, style = labelStyle ) Column(Modifier.padding(start = 16.dp)) { content() } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataValue.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow @Composable fun RowScope.DataValue( value: String, color: Color = Color.Unspecified, style: TextStyle = MaterialTheme.typography.titleSmall, clickable: Boolean = false, ) { SelectionContainer(Modifier.alignByBaseline()) { var clicked by remember { mutableStateOf(false) } val modifier = if (clickable) { Modifier .clickable { clicked = !clicked } .alignByBaseline() } else { Modifier .alignByBaseline() } Text( modifier = modifier, text = value, color = color, style = style, maxLines = if (clicked) Int.MAX_VALUE else 1, overflow = if (clicked) TextOverflow.Visible else TextOverflow.Ellipsis ) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @Composable fun DialogButton( buttonText: String, onClick: () -> Unit ) { TextButton( modifier = Modifier.padding(0.dp), shape = RoundedCornerShape(4.0.dp), contentPadding = PaddingValues( horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, vertical = ButtonDefaults.ContentPadding.calculateTopPadding() ), onClick = onClick ) { Text(buttonText, maxLines = 1, color = MaterialTheme.colorScheme.primary ) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import android.net.Uri import android.provider.OpenableColumns import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import com.github.capntrips.kernelflasher.MainActivity import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalAnimationApi @ExperimentalMaterialApi @ExperimentalMaterial3Api @ExperimentalSerializationApi @ExperimentalUnitApi @Composable fun FlashButton( buttonText: String, validExtension: String, callback: (uri: Uri) -> Unit ) { val mainActivity = LocalContext.current as MainActivity val result = remember { mutableStateOf(null) } val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { result.value = it if (it == null) { mainActivity.isAwaitingResult = false } } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { mainActivity.isAwaitingResult = true launcher.launch("*/*") } ) { Text(buttonText) } result.value?.let {uri -> if (mainActivity.isAwaitingResult) { val contentResolver = mainActivity.contentResolver val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor -> val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (nameIndex != -1 && cursor.moveToFirst()) { cursor.getString(nameIndex) } else { null } } if (fileName != null && fileName.endsWith(validExtension, ignoreCase = true)) { callback.invoke(uri) } else { // Invalid file extension, show an error message or handle it Toast.makeText(mainActivity.applicationContext, "Invalid file selected!", Toast.LENGTH_LONG).show() } } mainActivity.isAwaitingResult = false } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp @ExperimentalUnitApi @Composable fun ColumnScope.FlashList( cardTitle: String, output: List, content: @Composable ColumnScope.() -> Unit ) { val listState = rememberLazyListState() var hasDragged by remember { mutableStateOf(false) } val isDragged by listState.interactionSource.collectIsDraggedAsState() if (isDragged) { hasDragged = true } var shouldScroll = false if (!hasDragged) { if (listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != null) { if (listState.layoutInfo.totalItemsCount - listState.layoutInfo.visibleItemsInfo.size > listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index!!) { shouldScroll = true } } } LaunchedEffect(shouldScroll) { listState.animateScrollToItem(output.size) } DataCard (cardTitle) Spacer(Modifier.height(4.dp)) LazyColumn( Modifier .weight(1.0f) .fillMaxSize() .scrollbar(listState), listState ) { items(output) { message -> Text(message, style = LocalTextStyle.current.copy( fontFamily = FontFamily.Monospace, fontSize = TextUnit(12.0f, TextUnitType.Sp), lineHeight = TextUnit(18.0f, TextUnitType.Sp) ) ) } } content() } // https://stackoverflow.com/a/68056586/434343 fun Modifier.scrollbar( state: LazyListState, width: Dp = 6.dp ): Modifier = composed { var visibleItemsCountChanged = false var visibleItemsCount by remember { mutableIntStateOf(state.layoutInfo.visibleItemsInfo.size) } if (visibleItemsCount != state.layoutInfo.visibleItemsInfo.size) { visibleItemsCountChanged = true visibleItemsCount = state.layoutInfo.visibleItemsInfo.size } val hidden = state.layoutInfo.visibleItemsInfo.size == state.layoutInfo.totalItemsCount val targetAlpha = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0.5f else 0f val delay = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0 else 250 val duration = if (hidden || visibleItemsCountChanged) 0 else if (state.isScrollInProgress) 150 else 500 val alpha by animateFloatAsState( targetValue = targetAlpha, animationSpec = tween(delayMillis = delay, durationMillis = duration) ) drawWithContent { drawContent() val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index val needDrawScrollbar = state.isScrollInProgress || visibleItemsCountChanged || alpha > 0.0f if (needDrawScrollbar && firstVisibleElementIndex != null) { val elementHeight = this.size.height / state.layoutInfo.totalItemsCount val scrollbarOffsetY = firstVisibleElementIndex * elementHeight val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight drawRoundRect( color = Color.Gray, topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), size = Size(width.toPx(), scrollbarHeight), cornerRadius = CornerRadius(width.toPx(), width.toPx()), alpha = alpha ) } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Row import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel @ExperimentalMaterial3Api @Composable fun SlotCard( title: String, viewModel: SlotViewModel, navController: NavController, isSlotScreen: Boolean = false, showDlkm: Boolean = true, ) { DataCard ( title = "$title ${if(viewModel.isActive && viewModel.slotSuffix !="") "[${stringResource(R.string.active)}]" else ""}", button = { if (!isSlotScreen) { AnimatedVisibility(!viewModel.isRefreshing.value) { ViewButton { navController.navigate("slot${viewModel.slotSuffix}") } } } } ) { val cardWidth = remember { mutableIntStateOf(0) } if (!viewModel.sha1.isNullOrEmpty()) { DataRow( label = stringResource(R.string.boot_sha1), value = viewModel.sha1!!.substring(0, 8), valueStyle = MaterialTheme.typography.titleSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium ), mutableMaxWidth = cardWidth ) } AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.slotInfo.bootImgInfo.kernelVersion != null) { DataRow( label = stringResource(R.string.kernel_version), value = viewModel.slotInfo.bootImgInfo.kernelVersion ?: "", mutableMaxWidth = cardWidth, clickable = true ) } if (showDlkm && viewModel.hasVendorDlkm) { var vendorDlkmValue = stringResource(R.string.not_found) if (viewModel.isVendorDlkmMapped) { vendorDlkmValue = if (viewModel.isVendorDlkmMounted) { String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.mounted)) } else { String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.unmounted)) } } DataRow(stringResource(R.string.vendor_dlkm), vendorDlkmValue, mutableMaxWidth = cardWidth) } DataRow( label = stringResource(R.string.boot_fmt), value = viewModel.slotInfo.bootImgInfo.bootFmt ?: stringResource(R.string.not_found), mutableMaxWidth = cardWidth ) DataRow( label = if (viewModel.slotInfo.ramdiskInfo.ramdiskLocation == "init_boot.img") stringResource(R.string.init_boot_fmt) else if (viewModel.slotInfo.ramdiskInfo.ramdiskLocation == "vendor_boot.img") stringResource(R.string.vendor_boot_fmt) else stringResource(R.string.ramdisk_fmt), value = viewModel.slotInfo.ramdiskInfo.ramdiskFmt ?: stringResource(R.string.not_found), mutableMaxWidth = cardWidth ) if(isSlotScreen && viewModel.slotSuffix != "") { DataRow( label = stringResource(R.string.unbootable), value = viewModel.slotInfo.bootSlotInfo.unbootable ?: stringResource(R.string.not_found), mutableMaxWidth = cardWidth, valueColor = if (viewModel.slotInfo.bootSlotInfo.unbootable == "Yes") Color.Red else Color.Unspecified ) DataRow( label = stringResource(R.string.successful), value = viewModel.slotInfo.bootSlotInfo.successful ?: stringResource(R.string.not_found), mutableMaxWidth = cardWidth, valueColor = if (viewModel.slotInfo.bootSlotInfo.successful == "No") Color.Red else Color.Unspecified ) } if (!viewModel.isRefreshing.value && viewModel.hasError) { Row { DataValue( value = viewModel.error ?: "", color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.titleSmall, clickable = true ) } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt ================================================ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.github.capntrips.kernelflasher.R @Composable fun ViewButton( onClick: () -> Unit ) { TextButton( modifier = Modifier.padding(0.dp), shape = RoundedCornerShape(4.0.dp), contentPadding = PaddingValues( horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, vertical = ButtonDefaults.ContentPadding.calculateTopPadding() ), onClick = onClick ) { Text(stringResource(R.string.view), maxLines = 1) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.pullrefresh.PullRefreshIndicator import androidx.compose.material.pullrefresh.pullRefresh import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalMaterialApi @ExperimentalMaterial3Api @ExperimentalSerializationApi @Composable fun RefreshableScreen( viewModel: MainViewModel, navController: NavController, swipeEnabled: Boolean = false, content: @Composable ColumnScope.() -> Unit ) { val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues() val navigationBars = WindowInsets.navigationBars.asPaddingValues() val context = LocalContext.current val state = rememberPullRefreshState(viewModel.isRefreshing, onRefresh = { viewModel.refresh(context) }) Scaffold( topBar = { Box( Modifier .fillMaxWidth() .padding(statusBar)) { if (navController.previousBackStackEntry != null) { AnimatedVisibility( !viewModel.isRefreshing, enter = fadeIn(), exit = fadeOut() ) { IconButton( onClick = { navController.popBackStack() }, modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp) ) { Icon( Icons.Filled.ArrowBack, contentDescription = stringResource(R.string.back), tint = MaterialTheme.colorScheme.onSurface ) } } } Box( Modifier .fillMaxWidth() .padding(16.dp)) { Text( modifier = Modifier.align(Alignment.Center), text = stringResource(R.string.app_name), style = MaterialTheme.typography.headlineSmall ) } } } ) { paddingValues -> Box( modifier = Modifier .padding(paddingValues) .pullRefresh(state, swipeEnabled) .fillMaxSize(), ) { Column( modifier = Modifier .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding()) .fillMaxSize() .verticalScroll(rememberScrollState()), content = content ) PullRefreshIndicator( viewModel.isRefreshing, state = state, modifier = Modifier.align(Alignment.TopCenter), backgroundColor = MaterialTheme.colorScheme.background, contentColor = MaterialTheme.colorScheme.primaryContainer, scale = true ) } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.backups import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.PartitionUtil import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.DataSet import com.github.capntrips.kernelflasher.ui.components.ViewButton @ExperimentalMaterial3Api @Composable fun ColumnScope.BackupsContent( viewModel: BackupsViewModel, navController: NavController ) { val context = LocalContext.current val monoStyle = MaterialTheme.typography.titleSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium ) if (viewModel.currentBackup != null && viewModel.backups.containsKey(viewModel.currentBackup)) { DataCard (viewModel.currentBackup!!) { val cardWidth = remember { mutableIntStateOf(0) } val backupId = viewModel.currentBackup!! val currentBackup = viewModel.backups[backupId] if(currentBackup == null) return@DataCard DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth) DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) if (currentBackup.type == "raw") { currentBackup.bootSha1?.takeIf { it.length >= 8 }?.let { sha1 -> DataRow( label = stringResource(R.string.boot_sha1), value = sha1.substring(0, 8), valueStyle = monoStyle, mutableMaxWidth = cardWidth ) } if (currentBackup.hashes != null) { val hashWidth = remember { mutableIntStateOf(0) } DataSet(stringResource(R.string.hashes)) { for (partitionName in PartitionUtil.PartitionNames) { val hash = currentBackup.hashes[partitionName] if (hash != null) { DataRow( label = partitionName, value = hash.takeIf { it.isNotEmpty() }?.substring(0, 8) ?: "Hash not found!", valueStyle = monoStyle, mutableMaxWidth = hashWidth ) } } } } } } AnimatedVisibility(!viewModel.isRefreshing) { Column { Spacer(Modifier.height(5.dp)) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.delete(context) { navController.popBackStack() } } ) { Text(stringResource(R.string.delete)) } } } } else { DataCard(stringResource(R.string.backups)) AnimatedVisibility(viewModel.needsMigration) { Column { Spacer(Modifier.height(5.dp)) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.migrate(context) } ) { Text(stringResource(R.string.migrate)) } } } if (viewModel.backups.isNotEmpty()) { for (id in viewModel.backups.keys.sortedByDescending { it }) { val currentBackup = viewModel.backups[id]!! Spacer(Modifier.height(16.dp)) DataCard( title = id, button = { AnimatedVisibility(!viewModel.isRefreshing) { Column { ViewButton(onClick = { navController.navigate("backups/$id") }) } } } ) { val cardWidth = remember { mutableIntStateOf(0) } if (currentBackup.type == "raw") { currentBackup.bootSha1?.takeIf { it.length >= 8 }?.let { sha1 -> DataRow( label = stringResource(R.string.boot_sha1), value = sha1.substring(0, 8), valueStyle = monoStyle, mutableMaxWidth = cardWidth ) } } DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) } } } else { Spacer(Modifier.height(32.dp)) Text( stringResource(R.string.no_backups_found), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontStyle = FontStyle.Italic ) } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.backups import android.annotation.SuppressLint import android.content.Context import android.util.Log import android.widget.Toast import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import com.github.capntrips.kernelflasher.SharedViewModels import com.github.capntrips.kernelflasher.common.PartitionUtil import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.readText import com.github.capntrips.kernelflasher.common.types.backups.Backup import com.github.capntrips.kernelflasher.common.types.partitions.Partitions import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.ExtendedFile import com.topjohnwu.superuser.nio.FileSystemManager import kotlin.DeprecationLevel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import java.io.File import java.io.FileInputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.Properties class BackupsViewModel( context: Context, private val fileSystemManager: FileSystemManager, private val navController: NavController, private val _isRefreshing: MutableState, private val _backups: MutableMap ) : ViewModel() { companion object { const val TAG: String = "KernelFlasher/BackupsState" } private val _restoreOutput: SnapshotStateList = mutableStateListOf() var currentBackup: String? = null set(value) { if (value != field) { if (_backups[value]?.hashes != null) { PartitionUtil.AvailablePartitions.forEach { partitionName -> if (_backups[value]!!.hashes!![partitionName] != null) { _backupPartitions[partitionName] = true } } } field = value } } var wasRestored: Boolean? = null private val _backupPartitions: SnapshotStateMap = mutableStateMapOf() private val hashAlgorithm: String = "SHA-256" @Deprecated("Backup migration will be removed in the first stable release", level = DeprecationLevel.WARNING) private var _needsMigration: MutableState = mutableStateOf(false) val restoreOutput: List get() = _restoreOutput val backupPartitions: MutableMap get() = _backupPartitions val isRefreshing: Boolean get() = _isRefreshing.value val backups: Map get() = _backups @Deprecated("Backup migration will be removed in the first stable release") val needsMigration: Boolean get() = _needsMigration.value init { refresh(context) } fun refresh(context: Context) { val oldDir = context.getExternalFilesDir(null) val oldBackupsDir = File(oldDir, "backups") // Deprecated: Backup migration will be removed in the first stable release _needsMigration.value = oldBackupsDir.exists() && oldBackupsDir.listFiles()?.size!! > 0 @SuppressLint("SdCardPath") val externalDir = File("/sdcard/KernelFlasher") val backupsDir = fileSystemManager.getFile("$externalDir/backups") if (backupsDir.exists()) { val children = backupsDir.listFiles() if (children != null) { for (child in children.sortedByDescending{it.name}) { if (!child.isDirectory) { continue } val jsonFile = child.getChildFile("backup.json") if (jsonFile.exists()) { _backups[child.name] = Json.decodeFromString(jsonFile.readText()) } } } } } private fun launch(block: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { _isRefreshing.value = true try { block() } catch (e: Exception) { withContext (Dispatchers.Main) { Log.e(TAG, e.message, e) navController.navigate("error/${e.message}") { popUpTo("main") } } } _isRefreshing.value = false } } @Suppress("SameParameterValue") private fun log(context: Context, message: String, shouldThrow: Boolean = false) { Log.d(TAG, message) if (!shouldThrow) { viewModelScope.launch(Dispatchers.Main) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } else { throw Exception(message) } } fun clearCurrent() { currentBackup = null clearRestore() } private fun addMessage(message: String) { viewModelScope.launch(Dispatchers.Main) { _restoreOutput.add(message) } } @Suppress("FunctionName") private fun _clearRestore() { _restoreOutput.clear() wasRestored = null } private fun clearRestore() { _clearRestore() _backupPartitions.clear() } @Suppress("unused") @SuppressLint("SdCardPath") fun saveLog(context: Context) { launch { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) val log = File("/sdcard/Download/restore-log--$now.log") log.writeText(restoreOutput.joinToString("\n")) if (log.exists()) { log(context, "Saved restore log to $log") } else { log(context, "Failed to save $log", shouldThrow = true) } } } private fun restorePartitions(context: Context, source: ExtendedFile, slotSuffix: String): Partitions? { val partitions = HashMap() for (partitionName in PartitionUtil.PartitionNames) { if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) { val image = source.getChildFile("$partitionName.img") if (image.exists()) { val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) if (blockDevice != null && blockDevice.exists()) { addMessage("Restoring $partitionName") partitions[partitionName] = if (PartitionUtil.isPartitionLogical(context, partitionName)) { PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message -> addMessage(message) } } else { PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) } } else { log(context, "Partition $partitionName was not found", shouldThrow = true) } } } } if (partitions.isNotEmpty()) { return Partitions.from(partitions) } return null } fun restore(context: Context, slotSuffix: String) { launch { _clearRestore() @SuppressLint("SdCardPath") val externalDir = File("/sdcard/KernelFlasher") val backupsDir = fileSystemManager.getFile("$externalDir/backups") val backupDir = backupsDir.getChildFile(currentBackup!!) if (!backupDir.exists()) { log(context, "Backup $currentBackup does not exists", shouldThrow = true) return@launch } addMessage("Restoring backup $currentBackup") val hashes = restorePartitions(context, backupDir, slotSuffix) if (hashes == null) { log(context, "No partitions restored", shouldThrow = true) } addMessage("Backup $currentBackup restored") wasRestored = true } } fun delete(context: Context, callback: () -> Unit) { launch { @SuppressLint("SdCardPath") val externalDir = File("/sdcard/KernelFlasher") val backupsDir = fileSystemManager.getFile("$externalDir/backups") val backupDir = backupsDir.getChildFile(currentBackup!!) if (!backupDir.exists()) { log(context, "Backup $currentBackup does not exists", shouldThrow = true) return@launch } backupDir.deleteRecursively() _backups.remove(currentBackup!!) withContext(Dispatchers.Main) { callback.invoke() } } } @OptIn(ExperimentalSerializationApi::class) @SuppressLint("SdCardPath") @Deprecated("Backup migration will be removed in the first stable release") fun migrate(context: Context) { launch { val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher") if (!externalDir.exists()) { if (!externalDir.mkdir()) { log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true) } } val backupsDir = externalDir.getChildFile("backups") if (!backupsDir.exists()) { if (!backupsDir.mkdir()) { log(context, "Failed to create backups dir", shouldThrow = true) } } val oldDir = context.getExternalFilesDir(null) val oldBackupsDir = File(oldDir, "backups") if (oldBackupsDir.exists()) { val indentedJson = Json { prettyPrint = true } val children = oldBackupsDir.listFiles() if (children != null) { for (child in children.sortedByDescending{it.name}) { if (!child.isDirectory) { child.delete() continue } val propFile = File(child, "backup.prop") @Suppress("BlockingMethodInNonBlockingContext") val inputStream = FileInputStream(propFile) val props = Properties() @Suppress("BlockingMethodInNonBlockingContext") props.load(inputStream) val name = child.name val type = props.getProperty("type", "raw") val kernelVersion = props.getProperty("kernel") val bootSha1 = if (type == "raw") props.getProperty("sha1") else null val filename = if (type == "ak3") "ak3.zip" else null propFile.delete() val dest = backupsDir.getChildFile(child.name) Shell.cmd("mv $child $dest").exec() if (!dest.exists()) { throw Error("Too slow") } val jsonFile = dest.getChildFile("backup.json") val backup = Backup(name, type, kernelVersion, bootSha1, filename) jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } _backups[name] = backup } } oldBackupsDir.delete() } SharedViewModels.mainViewModel.markRefreshNeeded() refresh(context) } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.backups import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.PartitionUtil import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.DataSet import com.github.capntrips.kernelflasher.ui.components.FlashList import com.github.capntrips.kernelflasher.ui.components.SlotCard import com.github.capntrips.kernelflasher.ui.components.ViewButton import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel @ExperimentalMaterial3Api @ExperimentalUnitApi @Composable fun ColumnScope.SlotBackupsContent( slotViewModel: SlotViewModel, backupsViewModel: BackupsViewModel, slotSuffix: String, navController: NavController ) { val context = LocalContext.current val monoStyle = MaterialTheme.typography.titleSmall.copy( fontFamily = FontFamily.Monospace, fontWeight = FontWeight.Medium ) val currentRoute = navController.currentDestination?.route.orEmpty() if (!currentRoute.contains("/backups/{backupId}/restore")) { SlotCard( title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), viewModel = slotViewModel, navController = navController, isSlotScreen = true, showDlkm = false, ) Spacer(Modifier.height(16.dp)) if (backupsViewModel.currentBackup != null && backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) DataCard(backupsViewModel.currentBackup!!) { val cardWidth = remember { mutableIntStateOf(0) } DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth) DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) if (currentBackup.type == "raw") { currentBackup.bootSha1?.takeIf { it.length >= 8 }?.let { sha1 -> DataRow( label = stringResource(R.string.boot_sha1), value = sha1.substring(0, 8), valueStyle = monoStyle, mutableMaxWidth = cardWidth ) } if (currentBackup.hashes != null) { val hashWidth = remember { mutableIntStateOf(0) } DataSet(stringResource(R.string.hashes)) { for (partitionName in PartitionUtil.PartitionNames) { val hash = currentBackup.hashes[partitionName] if (hash != null) { DataRow( label = partitionName, value = hash.takeIf { it.isNotEmpty() && it.length >= 8 }?.substring(0, 8) ?: "Hash not found!", valueStyle = monoStyle, mutableMaxWidth = hashWidth ) } } } } } } AnimatedVisibility(!slotViewModel.isRefreshing.value) { Column { Spacer(Modifier.height(5.dp)) if (slotViewModel.isActive) { if (currentBackup.type == "raw") { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore") } ) { Text(stringResource(R.string.restore)) } } else if (currentBackup.type == "ak3") { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { slotViewModel.flashAk3(context, backupsViewModel.currentBackup!!, currentBackup.filename!!) navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { popUpTo("slot$slotSuffix") } } ) { Text(stringResource(R.string.flash)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { slotViewModel.flashAk3_mkbootfs(context, backupsViewModel.currentBackup!!, currentBackup.filename!!) navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { popUpTo("slot$slotSuffix") } } ) { Text(stringResource(R.string.flash_ak3_zip_mkbootfs)) } } } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { backupsViewModel.delete(context) { navController.popBackStack() } } ) { Text(stringResource(R.string.delete)) } } } } else { DataCard(stringResource(R.string.backups)) val backups = backupsViewModel.backups.filter { it.value.bootSha1.isNullOrEmpty() || it.value.bootSha1.equals(slotViewModel.sha1) || it.value.type == "ak3" } if (backups.isNotEmpty()) { for (id in backups.keys.sortedByDescending { it }) { Spacer(Modifier.height(16.dp)) DataCard( title = id, button = { AnimatedVisibility(!slotViewModel.isRefreshing.value) { ViewButton(onClick = { navController.navigate("slot$slotSuffix/backups/$id") }) } } ) { DataRow(stringResource(R.string.kernel_version), backups[id]!!.kernelVersion, clickable = true) } } } else { Spacer(Modifier.height(32.dp)) Text( stringResource(R.string.no_backups_found), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontStyle = FontStyle.Italic ) } } } else if (navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/restore")) { DataCard (stringResource(R.string.restore)) Spacer(Modifier.height(5.dp)) val disabledColor = ButtonDefaults.buttonColors( Color.Transparent, MaterialTheme.colorScheme.onSurface ) val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) if (currentBackup.hashes != null) { for (partitionName in PartitionUtil.PartitionNames) { val hash = currentBackup.hashes[partitionName] if (hash != null) { OutlinedButton( modifier = Modifier .fillMaxWidth() .alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), shape = RoundedCornerShape(4.dp), colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor, enabled = backupsViewModel.backupPartitions[partitionName] != null, onClick = { backupsViewModel.backupPartitions[partitionName] = !backupsViewModel.backupPartitions[partitionName]!! }, ) { Box(Modifier.fillMaxWidth()) { Checkbox(backupsViewModel.backupPartitions[partitionName] == true, null, Modifier .align(Alignment.CenterStart) .offset(x = -(16.dp))) Text(partitionName, Modifier.align(Alignment.Center)) } } } } } else { Text( stringResource(R.string.partition_selection_unavailable), modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, fontStyle = FontStyle.Italic ) Spacer(Modifier.height(5.dp)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { backupsViewModel.restore(context, slotSuffix) navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") { popUpTo("slot$slotSuffix") } }, enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none { currentBackup.hashes[it] != null && backupsViewModel.backupPartitions[it] == null } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty()) ) { Text(stringResource(R.string.restore)) } } else { FlashList( stringResource(R.string.restore), backupsViewModel.restoreOutput ) { AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) { Column { if (backupsViewModel.wasRestored != false) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("reboot") } ) { Text(stringResource(R.string.reboot)) } } } } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.error import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.github.capntrips.kernelflasher.ui.theme.Orange500 @ExperimentalMaterial3Api @Composable fun ErrorScreen(message: String) { Scaffold { paddingValues -> Box( contentAlignment = Alignment.Center, modifier = Modifier .padding(paddingValues) .fillMaxSize() ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Icon( Icons.Filled.Warning, modifier = Modifier .width(48.dp) .height(48.dp), tint = Orange500, contentDescription = message ) Spacer(Modifier.height(8.dp)) Text( message, modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp), style = MaterialTheme.typography.titleLarge, ) } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.main import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.SlotCard import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalMaterial3Api @ExperimentalSerializationApi @Composable fun ColumnScope.MainContent( viewModel: MainViewModel, navController: NavController ) { val context = LocalContext.current DataCard (title = stringResource(R.string.device)) { val cardWidth = remember { mutableIntStateOf(0) } DataRow(stringResource(R.string.model), "${Build.MODEL} (${Build.DEVICE})", mutableMaxWidth = cardWidth) DataRow(stringResource(R.string.build_number), Build.ID, mutableMaxWidth = cardWidth) DataRow(stringResource(R.string.kernel_version), viewModel.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) if (viewModel.isAb) DataRow(stringResource(R.string.slot_suffix), viewModel.slotSuffix, mutableMaxWidth = cardWidth) if(viewModel.susfsVersion != "v0.0.0" && viewModel.susfsVersion != "Invalid") DataRow(stringResource(R.string.susfs_version), viewModel.susfsVersion, mutableMaxWidth = cardWidth) if(viewModel.halInfo != "") DataRow("Boot HAL version", viewModel.halInfo, mutableMaxWidth = cardWidth) } Spacer(Modifier.height(16.dp)) SlotCard( title = stringResource(if (viewModel.isAb) R.string.slot_a else R.string.slot), viewModel = viewModel.slotA, navController = navController ) if (viewModel.isAb) { Spacer(Modifier.height(16.dp)) SlotCard( title = stringResource(R.string.slot_b), viewModel = viewModel.slotB!!, navController = navController ) } Spacer(Modifier.height(16.dp)) AnimatedVisibility(!viewModel.isRefreshing) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("backups") } ) { Text(stringResource(R.string.backups)) } } AnimatedVisibility(!viewModel.isRefreshing) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("updates") } ) { Text(stringResource(R.string.updates)) } } if (viewModel.hasRamoops) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.saveRamoops(context) } ) { Text(stringResource(R.string.save_ramoops)) } } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.saveDmesg(context) } ) { Text(stringResource(R.string.save_dmesg)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.saveLogcat(context) } ) { Text(stringResource(R.string.save_logcat)) } AnimatedVisibility(!viewModel.isRefreshing) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("reboot") } ) { Text(stringResource(R.string.reboot)) } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.main import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.util.Log import android.widget.Toast import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import com.github.capntrips.kernelflasher.common.PartitionUtil import com.github.capntrips.kernelflasher.common.types.backups.Backup import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsViewModel import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootViewModel import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewModel import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.FileSystemManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import java.io.File import java.time.LocalDateTime import java.time.format.DateTimeFormatter @ExperimentalSerializationApi class MainViewModel( context: Context, fileSystemManager: FileSystemManager, private val navController: NavController ) : ViewModel() { companion object { const val TAG: String = "KernelFlasher/MainViewModel" } val slotSuffix: String val kernelVersion: String val halInfo: String val susfsVersion: String val isAb: Boolean val slotA: SlotViewModel val slotB: SlotViewModel? val backups: BackupsViewModel val updates: UpdatesViewModel val reboot: RebootViewModel val hasRamoops: Boolean private val _isRefreshing: MutableState = mutableStateOf(true) private val _isRefreshRequired = mutableStateOf(true) private var _error: String? = null private var _backups: MutableMap = mutableMapOf() var showSlotIntentDialog: MutableState = mutableStateOf(false) var pendingFlashUri: Uri? = null var slotSuffixForFlash = mutableStateOf(null) val isRefreshing: Boolean get() = _isRefreshing.value val isRefreshRequired: Boolean get() = _isRefreshRequired.value val hasError: Boolean get() = _error != null val error: String get() = _error!! fun markRefreshNeeded() { _isRefreshRequired.value = true } data class UpdateDialogData( val title: String, val changelog: List, val onConfirm: () -> Unit ) var updateDialogData by mutableStateOf(null) private set fun showUpdateDialog(title: String, changelog: List, onConfirm: () -> Unit) { updateDialogData = UpdateDialogData(title, changelog, onConfirm) } fun hideUpdateDialog() { updateDialogData = null } init { PartitionUtil.init(context, fileSystemManager) val bootctl = File(context.filesDir, "bootctl") halInfo = runCatching { Shell.cmd("$bootctl hal-info").exec().out[0].substringAfter("HAL Version: ").trim() } .recoverCatching { "" } .getOrDefault("") kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0] susfsVersion = runCatching { Shell.cmd("susfsd version").exec().out[0] } .recoverCatching { Shell.cmd("ksu_susfs show version").exec().out[0] } .getOrDefault("v0.0.0") slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] backups = BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups) updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing) reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing) // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:bootable/recovery/recovery.cpp;l=320 isAb = slotSuffix.isNotEmpty() if (isAb) { val bootA = PartitionUtil.findPartitionBlockDevice(context, "boot", "_a")!! val bootB = PartitionUtil.findPartitionBlockDevice(context, "boot", "_b")!! val initBootA = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_a") val initBootB = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_b") slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_a", "_a", bootA, initBootA, _backups) slotB = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_b", "_b", bootB, initBootB, _backups) } else { val boot = PartitionUtil.findPartitionBlockDevice(context, "boot", "")!! val initBoot = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "") slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, true, "", boot, initBoot, _backups) if (slotA.hasError) { _error = slotA.error } slotB = null } hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists() _isRefreshing.value = false _isRefreshRequired.value = false } fun refresh(context: Context) { if (!isRefreshRequired) return launch { slotA.refresh(context) if (isAb) { slotB!!.refresh(context) } backups.refresh(context) _isRefreshRequired.value = false } } private fun launch(block: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.Main) { _isRefreshing.value = true } try { block() } catch (e: Exception) { withContext (Dispatchers.Main) { Log.e(TAG, e.message, e) navController.navigate("error/${e.message}") { popUpTo("main") } } } viewModelScope.launch(Dispatchers.Main) { _isRefreshing.value = false } } } @Suppress("SameParameterValue") private fun log(context: Context, message: String, shouldThrow: Boolean = false) { Log.d(TAG, message) if (!shouldThrow) { viewModelScope.launch(Dispatchers.Main) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } else { throw Exception(message) } } fun saveRamoops(context: Context) { launch { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) @SuppressLint("SdCardPath") val ramoops = File("/sdcard/Download/console-ramoops--$now.log") Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec() if (ramoops.exists()) { log(context, "Saved ramoops to $ramoops") } else { log(context, "Failed to save $ramoops", shouldThrow = true) } } } fun saveDmesg(context: Context) { launch { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) @SuppressLint("SdCardPath") val dmesg = File("/sdcard/Download/dmesg--$now.log") Shell.cmd("dmesg > $dmesg").exec() if (dmesg.exists()) { log(context, "Saved dmesg to $dmesg") } else { log(context, "Failed to save $dmesg", shouldThrow = true) } } } fun saveLogcat(context: Context) { launch { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) @SuppressLint("SdCardPath") val logcat = File("/sdcard/Download/logcat--$now.log") Shell.cmd("logcat -d > $logcat").exec() if (logcat.exists()) { log(context, "Saved logcat to $logcat") } else { log(context, "Failed to save $logcat", shouldThrow = true) } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.reboot import android.os.Build import android.os.PowerManager import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R @Suppress("UnusedReceiverParameter") @Composable fun ColumnScope.RebootContent( viewModel: RebootViewModel, @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController ) { val context = LocalContext.current OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.rebootSystem() } ) { Text(stringResource(R.string.reboot)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.rebootRecovery() } ) { Text(stringResource(R.string.reboot_recovery)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.rebootBootloader() } ) { Text(stringResource(R.string.reboot_bootloader)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.rebootDownload() } ) { Text(stringResource(R.string.reboot_download)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.rebootEdl() } ) { Text(stringResource(R.string.reboot_edl)) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.reboot import android.content.Context import android.util.Log import androidx.compose.runtime.MutableState import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.FileSystemManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class RebootViewModel( @Suppress("UNUSED_PARAMETER") ignoredContext: Context, @Suppress("unused") private val fileSystemManager: FileSystemManager, private val navController: NavController, private val _isRefreshing: MutableState ) : ViewModel() { companion object { const val TAG: String = "KernelFlasher/RebootState" } val isRefreshing: Boolean get() = _isRefreshing.value private fun launch(block: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { _isRefreshing.value = true try { block() } catch (e: Exception) { withContext (Dispatchers.Main) { Log.e(TAG, e.message, e) navController.navigate("error/${e.message}") { popUpTo("main") } } } _isRefreshing.value = false } } private fun reboot(destination: String = "") { launch { // https://github.com/topjohnwu/Magisk/blob/v25.2/app/src/main/java/com/topjohnwu/magisk/ktx/XSU.kt#L11-L15 if (destination == "recovery") { // https://github.com/topjohnwu/Magisk/pull/5637 Shell.cmd("/system/bin/input keyevent 26").submit() } Shell.cmd("/system/bin/svc power reboot $destination || /system/bin/reboot $destination").submit() } } fun rebootSystem() { reboot() } fun rebootRecovery() { reboot("recovery") } fun rebootBootloader() { reboot("bootloader") } fun rebootDownload() { reboot("download") } fun rebootEdl() { reboot("edl") } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.slot import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.ui.components.SlotCard @ExperimentalAnimationApi @ExperimentalMaterial3Api @ExperimentalUnitApi @Composable fun ColumnScope.SlotContent( viewModel: SlotViewModel, slotSuffix: String, navController: NavController ) { val context = LocalContext.current SlotCard( title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), viewModel = viewModel, navController = navController, isSlotScreen = true ) AnimatedVisibility(!viewModel.isRefreshing.value) { Column { Spacer(Modifier.height(5.dp)) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("slot$slotSuffix/flash") } ) { Text(stringResource(R.string.flash)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.clearFlash(context) navController.navigate("slot$slotSuffix/backup") } ) { Text(stringResource(R.string.backup)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("slot$slotSuffix/backups") } ) { Text(stringResource(R.string.restore)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { if (!viewModel.isRefreshing.value) viewModel.getKernel(context) } ) { Text(stringResource(R.string.check_kernel_version)) } if (viewModel.hasVendorDlkm) { AnimatedVisibility(!viewModel.isRefreshing.value) { AnimatedVisibility(viewModel.isVendorDlkmMounted) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.unmountVendorDlkm(context) } ) { Text(stringResource(R.string.unmount_vendor_dlkm)) } } AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) { Column { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.mountVendorDlkm(context) } ) { Text(stringResource(R.string.mount_vendor_dlkm)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.unmapVendorDlkm(context) } ) { Text(stringResource(R.string.unmap_vendor_dlkm)) } } } AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.mapVendorDlkm(context) } ) { Text(stringResource(R.string.map_vendor_dlkm)) } } } } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.slot import android.provider.OpenableColumns import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import androidx.compose.ui.text.font.FontWeight import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.PartitionUtil import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.FlashButton import com.github.capntrips.kernelflasher.ui.components.FlashList import com.github.capntrips.kernelflasher.ui.components.SlotCard import com.github.capntrips.kernelflasher.ui.components.DialogButton import kotlinx.serialization.ExperimentalSerializationApi import java.io.File @ExperimentalAnimationApi @ExperimentalMaterialApi @ExperimentalMaterial3Api @ExperimentalUnitApi @ExperimentalSerializationApi @Composable fun ColumnScope.SlotFlashContent( viewModel: SlotViewModel, slotSuffix: String, navController: NavController ) { val context = LocalContext.current val isRefreshing by remember { derivedStateOf { viewModel.isRefreshing } } val currentRoute = navController.currentDestination?.route.orEmpty() val isAk3 = currentRoute.contains("ak3") val isFlashImage = currentRoute.endsWith("/flash/image") val isBackup = currentRoute.endsWith("/backup") val isBackupResult = currentRoute.endsWith("/backup/backup") val isFlashAk3 = currentRoute.endsWith("/flash/ak3") val isImageFlashResult = currentRoute.endsWith("/flash/image/flash") val isFlashScreen = currentRoute.endsWith("/flash") val isSlotScreen = !(isFlashAk3 || isImageFlashResult || isBackupResult) // Not in Flashing Screen; So Its considered Slot Screen BackHandler(enabled = ((isFlashAk3 || isImageFlashResult || isBackupResult) && isRefreshing.value)) { } if (isSlotScreen) { SlotCard( title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot), viewModel = viewModel, navController = navController, isSlotScreen = true, showDlkm = false ) Spacer(Modifier.height(16.dp)) if (isFlashScreen) { DataCard (stringResource(R.string.flash)) Spacer(Modifier.height(5.dp)) FlashButton(stringResource(R.string.flash_ak3_zip), "zip" ,callback = { uri -> viewModel.flashActionType = "flashAk3" viewModel.flashActionURI = uri viewModel.showConfirmDialog() }) FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip" ,callback = { uri -> viewModel.flashActionType = "flashAk3_mkbootfs" viewModel.flashActionURI = uri viewModel.showConfirmDialog() }) FlashButton(stringResource(R.string.flash_ksu_lkm), "ko" ,callback = { uri -> viewModel.flashActionType = "flashKsuDriver" viewModel.flashActionURI = uri viewModel.showConfirmDialog() }) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("slot$slotSuffix/flash/image") } ) { Text(stringResource(R.string.flash_partition_image)) } } else if (isFlashImage) { DataCard (stringResource(R.string.flash_partition_image)) Spacer(Modifier.height(5.dp)) for (partitionName in PartitionUtil.AvailablePartitions) { FlashButton(partitionName, "img" ,callback = { uri -> viewModel.flashActionType = "flashImage" viewModel.flashActionURI = uri viewModel.flashActionPartName = partitionName viewModel.showConfirmDialog() }) } } else if (isBackup) { DataCard (stringResource(R.string.backup)) Spacer(Modifier.height(5.dp)) val disabledColor = ButtonDefaults.buttonColors( Color.Transparent, MaterialTheme.colorScheme.onSurface ) for (partitionName in PartitionUtil.AvailablePartitions) { OutlinedButton( modifier = Modifier .fillMaxWidth() .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), shape = RoundedCornerShape(4.dp), colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor, onClick = { viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!! }, ) { Box(Modifier.fillMaxWidth()) { Checkbox(viewModel.backupPartitions[partitionName]!!, null, Modifier .align(Alignment.CenterStart) .offset(x = -(16.dp))) Text(partitionName, Modifier.align(Alignment.Center)) } } } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.backup(context) navController.navigate("slot$slotSuffix/backup/backup") { popUpTo("slot$slotSuffix") } }, enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty() ) { Text(stringResource(R.string.backup_now)) } } } else { Text("") FlashList( stringResource(if (isBackupResult) R.string.backup else R.string.flash), if (isAk3) viewModel.uiPrintedOutput else viewModel.flashOutput ) { AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.wasFlashSuccess.value != null) { Column { if (isAk3) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.saveLog(context) } ) { Text(stringResource(R.string.save_ak3_log)) } } if (isAk3) { AnimatedVisibility(!currentRoute.endsWith("/backups/{backupId}/flash/ak3") && viewModel.wasFlashSuccess.value != false) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.backupZip(context) { navController.navigate("slot$slotSuffix/backups") { popUpTo("slot$slotSuffix") } } } ) { Text(stringResource(R.string.save_ak3_zip_as_backup)) } } } if (viewModel.wasFlashSuccess.value == true && viewModel.showCautionDialog == true){ AlertDialog( onDismissRequest = { viewModel.hideCautionDialog() }, title = { Text("CAUTION!", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("You have flashed to inactive slot!", fontWeight = FontWeight.Bold) Text("But the active slot is not changed after flashing.", fontWeight = FontWeight.Bold) Text("Change active slot or return to System Updater to complete OTA.", fontWeight = FontWeight.Bold) Text("Do not reboot from here, unless you know what you are doing.", fontWeight = FontWeight.Bold) } }, confirmButton = { DialogButton( "CHANGE SLOT" ) { viewModel.hideCautionDialog() viewModel.switchSlot(context) } }, dismissButton = { DialogButton( "CANCEL" ) { viewModel.hideCautionDialog() } }, modifier = Modifier.padding(16.dp) ) } if (viewModel.wasFlashSuccess.value != false && isBackupResult) { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.popBackStack() } ) { Text(stringResource(R.string.back)) } } else { OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("reboot") } ) { Text(stringResource(R.string.reboot)) } } } } } } if(viewModel.showConfirmDialog == true) { var filename = when { viewModel.flashActionURI?.scheme == "file" -> { File(viewModel.flashActionURI?.path ?: "").name } viewModel.flashActionURI != null -> { context.contentResolver.query(viewModel.flashActionURI!!, null, null, null, null)?.use { cursor -> val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (cursor.moveToFirst() && nameIndex != -1) { cursor.getString(nameIndex) } else null } ?: "Unable to determine filename!" } else -> "Unable to determine filename!" } AlertDialog( onDismissRequest = { viewModel.hideConfirmDialog() }, title = { Text("CAUTION!", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) }, text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Are you Sure you want to flash this file?", fontWeight = FontWeight.Bold) Text("", fontWeight = FontWeight.Bold) Text("$filename", fontWeight = FontWeight.Bold) } }, confirmButton = { DialogButton( "Flash" ) { viewModel.hideConfirmDialog() val isOtherFlash = viewModel.flashActionType != "flashImage" && viewModel.flashActionURI != null val isPartitionFlash = viewModel.flashActionType == "flashImage" && viewModel.flashActionPartName != null && viewModel.flashActionURI != null if (isOtherFlash || isPartitionFlash) { val uri = viewModel.flashActionURI!! val partitionName: String? = viewModel.flashActionPartName when (viewModel.flashActionType) { "flashAk3" -> { navController.navigate("slot$slotSuffix/flash/ak3") { popUpTo("slot$slotSuffix") } viewModel.flashAk3(context, uri) } "flashAk3_mkbootfs" -> { navController.navigate("slot$slotSuffix/flash/ak3") { popUpTo("slot$slotSuffix") } viewModel.flashAk3_mkbootfs(context, uri) } "flashKsuDriver" -> { navController.navigate("slot$slotSuffix/flash/image/flash") { popUpTo("slot$slotSuffix") } viewModel.flashKsuDriver(context, uri) } "flashImage" -> { navController.navigate("slot$slotSuffix/flash/image/flash") { popUpTo("slot$slotSuffix") } viewModel.flashImage( context, uri, partitionName!! ) } } viewModel.flashActionType = "" viewModel.flashActionURI = null viewModel.flashActionPartName = null } } }, dismissButton = { DialogButton( "CANCEL" ) { viewModel.hideConfirmDialog() } }, modifier = Modifier.padding(16.dp) ) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.slot import android.annotation.SuppressLint import android.content.Context import android.net.Uri import android.provider.OpenableColumns import android.util.Log import android.widget.Toast import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import com.github.capntrips.kernelflasher.SharedViewModels import com.github.capntrips.kernelflasher.common.PartitionUtil import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.inputStream import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream import com.github.capntrips.kernelflasher.common.types.backups.Backup import com.github.capntrips.kernelflasher.common.types.partitions.Partitions import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.nio.ExtendedFile import com.topjohnwu.superuser.nio.FileSystemManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import java.io.File import java.security.DigestOutputStream import java.security.MessageDigest import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.zip.ZipFile class SlotViewModel( context: Context, private val fileSystemManager: FileSystemManager, private val navController: NavController, private val _isRefreshing: MutableState, val isActive: Boolean, val slotSuffix: String, val boot: File, val initBoot: File?, private val _backups: MutableMap ) : ViewModel() { companion object { const val TAG: String = "KernelFlasher/SlotState" const val HEADER_VER = "HEADER_VER" const val KERNEL_FMT = "KERNEL_FMT" const val RAMDISK_FMT = "RAMDISK_FMT" const val VND_RAMDISK = "VND_RAMDISK" } data class BootSlotInfo( var unbootable: String? = null, var successful: String? = null, ) data class BootImgInfo( var kernelVersion: String? = null, var bootFmt: String? = null, var headerVersion: String? = null, ) data class RamdiskInfo( var headerVersion: String? = null, var ramdiskFmt: String? = null, var ramdiskLocation: String? = null, ) data class SlotInfo( var bootSlotInfo: BootSlotInfo, var bootImgInfo: BootImgInfo, var ramdiskInfo: RamdiskInfo, ) private var _sha1: String? = null private val _slotInfo: MutableState = mutableStateOf(SlotInfo(BootSlotInfo(), BootImgInfo(), RamdiskInfo())) var hasVendorDlkm: Boolean = false var isVendorDlkmMapped: Boolean = false var isVendorDlkmMounted: Boolean = false private val _flashOutput: SnapshotStateList = mutableStateListOf() private val _wasFlashSuccess: MutableState = mutableStateOf(null) private val _backupPartitions: SnapshotStateMap = mutableStateMapOf() private var wasSlotReset: Boolean = false private var flashUri: Uri? = null private var flashFilename: String? = null private val hashAlgorithm: String = "SHA-256" private var inInit = true private var _error: String? = null private val _showCautionDialog: MutableState = mutableStateOf(false) private val _showConfirmDialog: MutableState = mutableStateOf(false) var flashActionType: String = "" var flashActionURI: Uri? = null var flashActionPartName: String? = null val sha1: String? get() = _sha1 val flashOutput: List get() = _flashOutput val uiPrintedOutput: List get() = _flashOutput.filter { it.startsWith("ui_print") }.map { it.substringAfter("ui_print").trim() }.filter { it.isNotEmpty() || it == "" } val wasFlashSuccess: MutableState get() = _wasFlashSuccess val backupPartitions: MutableMap get() = _backupPartitions val isRefreshing: MutableState get() = _isRefreshing val hasError: Boolean get() = _error != null val error: String? get() = _error val showCautionDialog: Boolean get() = _showCautionDialog.value val showConfirmDialog: Boolean get() = _showConfirmDialog.value val slotInfo: SlotInfo get() = _slotInfo.value init { refresh(context) } private fun extractKernelValues(input: String, key: String, isVendor_boot: Boolean = false): String? { val regex = if(isVendor_boot == true) Regex("VND_RAMDISK.*fmt=\\[([^]]+)]") else Regex("$key\\s*\\[([^]]+)]") return regex.find(input)?.groupValues?.get(1) } fun refresh(context: Context) { _error = null _sha1 = null _slotInfo.value.bootSlotInfo = _slotInfo.value.bootSlotInfo.copy(null, null) _slotInfo.value.bootImgInfo = _slotInfo.value.bootImgInfo.copy(null, null, null) _slotInfo.value.ramdiskInfo = _slotInfo.value.ramdiskInfo.copy(null, null, null) if (!isActive) { inInit = true } val magiskboot = File(context.filesDir, "magiskboot") val bootctl = File(context.filesDir, "bootctl") Shell.cmd("$magiskboot cleanup").exec() val unpackBootOutput = mutableListOf() Shell.cmd("$magiskboot unpack $boot").to(unpackBootOutput, unpackBootOutput).exec() val bootUnpackOp = unpackBootOutput.joinToString("\n") if(slotSuffix != "") { val resCode1 = Shell.cmd("$bootctl is-slot-bootable " + if (slotSuffix == "_a") "0" else "1").exec().code _slotInfo.value.bootSlotInfo.unbootable = if(resCode1 == 0) "No" else "Yes" val resCode2 = Shell.cmd("$bootctl is-slot-marked-successful " + if (slotSuffix == "_a") "0" else "1").exec().code _slotInfo.value.bootSlotInfo.successful = if(resCode2 == 0) "Yes" else "No" } _slotInfo.value.bootImgInfo.headerVersion = extractKernelValues(bootUnpackOp.trimIndent(), HEADER_VER) _slotInfo.value.bootImgInfo.bootFmt = extractKernelValues(bootUnpackOp.trimIndent(), KERNEL_FMT) _slotInfo.value.ramdiskInfo.ramdiskFmt = extractKernelValues(bootUnpackOp.trimIndent(), RAMDISK_FMT) if (_slotInfo.value.ramdiskInfo.ramdiskFmt != null) { _slotInfo.value.ramdiskInfo.ramdiskLocation = "boot.img" _slotInfo.value.ramdiskInfo.headerVersion = _slotInfo.value.bootImgInfo.headerVersion } Log.d(TAG, _slotInfo.value.bootImgInfo.toString()) if (initBoot != null && _slotInfo.value.ramdiskInfo.ramdiskFmt == null) { val unpackInitBootOutput = mutableListOf() if(Shell.cmd("$magiskboot unpack $initBoot").to(unpackInitBootOutput, unpackInitBootOutput).exec().isSuccess) { val initBootUnpackOp = unpackInitBootOutput.joinToString("\n") _slotInfo.value.ramdiskInfo.ramdiskFmt = extractKernelValues(initBootUnpackOp.trimIndent(), RAMDISK_FMT) _slotInfo.value.ramdiskInfo.ramdiskLocation = "init_boot.img" } } else { var vendor_boot = PartitionUtil.findPartitionBlockDevice(context, "vendor_boot", slotSuffix) val unpackVendorBootOutput = mutableListOf() if(Shell.cmd("$magiskboot unpack $vendor_boot").to(unpackVendorBootOutput, unpackVendorBootOutput).exec().isSuccess) { val vendorBootUnpackOp = unpackVendorBootOutput.joinToString("\n") _slotInfo.value.ramdiskInfo.ramdiskFmt = extractKernelValues(vendorBootUnpackOp.trimIndent(), VND_RAMDISK, true) _slotInfo.value.ramdiskInfo.ramdiskLocation = "vendor_boot.img" } } val ramdisk = File(context.filesDir, "ramdisk.cpio") val kernel = File(context.filesDir, "kernel") var vendorDlkm = PartitionUtil.findPartitionBlockDevice(context, "vendor_dlkm", slotSuffix) hasVendorDlkm = vendorDlkm != null if (hasVendorDlkm) { isVendorDlkmMapped = vendorDlkm?.exists() == true if (isVendorDlkmMapped) { isVendorDlkmMounted = isPartitionMounted(vendorDlkm!!) if (!isVendorDlkmMounted) { vendorDlkm = fileSystemManager.getFile("/dev/block/mapper/vendor_dlkm-verity") isVendorDlkmMounted = isPartitionMounted(vendorDlkm) } } else { isVendorDlkmMounted = false } } if (ramdisk.exists()) { when (Shell.cmd("$magiskboot cpio ramdisk.cpio test").exec().code) { 0 -> _sha1 = Shell.cmd("$magiskboot sha1 $boot").exec().out.firstOrNull() 1 -> _sha1 = Shell.cmd("$magiskboot cpio ramdisk.cpio sha1").exec().out.firstOrNull() else -> _error = "Invalid ramdisk in boot.img" } } else if (kernel.exists()) { _sha1 = Shell.cmd("$magiskboot sha1 $boot").exec().out.firstOrNull() if(_slotInfo.value.bootImgInfo.headerVersion.equals("4") && _slotInfo.value.ramdiskInfo.ramdiskLocation.equals(null)) { _slotInfo.value.ramdiskInfo.ramdiskLocation = "boot.img" _slotInfo.value.ramdiskInfo.ramdiskFmt = "lz4_legacy" } } else { if(_slotInfo.value.bootImgInfo.headerVersion.equals("4") && _slotInfo.value.ramdiskInfo.ramdiskLocation.equals(null)) { _slotInfo.value.ramdiskInfo.ramdiskLocation = "boot.img" _slotInfo.value.ramdiskInfo.ramdiskFmt = "lz4_legacy" } _error = "Unable to generate SHA1 hash. Invalid boot.img or magiskboot unpack failed!" } Shell.cmd("$magiskboot cleanup").exec() PartitionUtil.AvailablePartitions.forEach { partitionName -> _backupPartitions[partitionName] = true } _slotInfo.value.bootImgInfo.kernelVersion = null inInit = false } // TODO: use base class for common functions private fun launch(block: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { _isRefreshing.value = true try { block() } catch (e: Exception) { withContext (Dispatchers.Main) { Log.e(TAG, e.message, e) navController.navigate("error/${e.message}") { popUpTo("main") } } } _isRefreshing.value = false } } private fun showCautionDialog() { _showCautionDialog.value = true } fun hideCautionDialog() { _showCautionDialog.value = false } fun showConfirmDialog() { _showConfirmDialog.value = true } fun hideConfirmDialog() { _showConfirmDialog.value = false } // TODO: use base class for common functions @Suppress("SameParameterValue") private fun log(context: Context, message: String, shouldThrow: Boolean = false) { Log.d(TAG, message) if (!shouldThrow) { viewModelScope.launch(Dispatchers.Main) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } else { if (inInit) { _error = message } else { throw Exception(message) } } } @Suppress("SameParameterValue") private fun uiPrint(message: String) { viewModelScope.launch(Dispatchers.Main) { _flashOutput.add("ui_print $message") } } // TODO: use base class for common functions private fun addMessage(message: String) { viewModelScope.launch(Dispatchers.Main) { _flashOutput.add(message) } } private fun clearTmp(context: Context) { if (flashFilename != null) { val zip = File(context.filesDir, flashFilename!!) if (zip.exists()) { zip.delete() } } } @Suppress("FunctionName") private fun _clearFlash() { _flashOutput.clear() _wasFlashSuccess.value = null } fun clearFlash(context: Context) { _clearFlash() PartitionUtil.AvailablePartitions.forEach { partitionName -> _backupPartitions[partitionName] = true } launch { clearTmp(context) } } // TODO: use base class for common functions @SuppressLint("SdCardPath") fun saveLog(context: Context) { launch { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) val logName = if (navController.currentDestination!!.route!!.contains("ak3")) { "ak3" } else if (navController.currentDestination!!.route!!.endsWith("/backup")) { "backup" } else { "flash" } val log = File("/sdcard/Download/$logName-log--$now.log") if (navController.currentDestination!!.route!!.contains("ak3")) { log.writeText(flashOutput.filter { !it.matches("""progress [\d.]* [\d.]*""".toRegex()) }.joinToString("\n").replace("""ui_print (.*)\n {6}ui_print""".toRegex(), "$1")) } else { log.writeText(flashOutput.joinToString("\n")) } if (log.exists()) { log(context, "Saved $logName log to $log") } else { log(context, "Failed to save $log", shouldThrow = true) } } } @Suppress("FunctionName", "SameParameterValue") private fun _getKernel(context: Context) { val magiskboot = File(context.filesDir, "magiskboot") Shell.cmd("$magiskboot unpack $boot").exec() val kernel = File(context.filesDir, "kernel") if (kernel.exists()) { val result = Shell.cmd("strings kernel | grep -E -m1 'Linux version.*#' | cut -d\\ -f3-").exec().out if (result.isNotEmpty()) { _slotInfo.value.bootImgInfo.kernelVersion = result[0].replace("""\(.+\)""".toRegex(), "").replace("""\s+""".toRegex(), " ") } } Shell.cmd("$magiskboot cleanup").exec() } fun getKernel(context: Context) { launch { _getKernel(context) } } private fun isPartitionMounted(partition: File): Boolean { @Suppress("LiftReturnOrAssignment") if (partition.exists()) { val dmPath = Shell.cmd("readlink -f $partition").exec().out[0] val mounts = Shell.cmd("mount | grep -w $dmPath").exec().out return mounts.isNotEmpty() } else { return false } } @OptIn(ExperimentalSerializationApi::class) fun unmountVendorDlkm(context: Context) { launch { val httools = File(context.filesDir, "httools_static") Shell.cmd("$httools umount vendor_dlkm").exec() SharedViewModels.mainViewModel.markRefreshNeeded() refresh(context) } } @OptIn(ExperimentalSerializationApi::class) fun mountVendorDlkm(context: Context) { launch { val httools = File(context.filesDir, "httools_static") Shell.cmd("$httools mount vendor_dlkm").exec() SharedViewModels.mainViewModel.markRefreshNeeded() refresh(context) } } @OptIn(ExperimentalSerializationApi::class) fun unmapVendorDlkm(context: Context) { launch { val lptools = File(context.filesDir, "lptools_static") val mapperDir = "/dev/block/mapper" val vendorDlkm = fileSystemManager.getFile(mapperDir, "vendor_dlkm$slotSuffix") if (vendorDlkm.exists()) { val vendorDlkmVerity = fileSystemManager.getFile(mapperDir, "vendor_dlkm-verity") if (vendorDlkmVerity.exists()) { Shell.cmd("$lptools unmap vendor_dlkm-verity").exec() } else { Shell.cmd("$lptools unmap vendor_dlkm$slotSuffix").exec() } } SharedViewModels.mainViewModel.markRefreshNeeded() refresh(context) } } @OptIn(ExperimentalSerializationApi::class) fun mapVendorDlkm(context: Context) { launch { val lptools = File(context.filesDir, "lptools_static") Shell.cmd("$lptools map vendor_dlkm$slotSuffix").exec() SharedViewModels.mainViewModel.markRefreshNeeded() refresh(context) } } private fun backupPartition(partition: ExtendedFile, destination: ExtendedFile): String? { if (partition.exists()) { val messageDigest = MessageDigest.getInstance(hashAlgorithm) partition.inputStream().use { inputStream -> destination.outputStream().use { outputStream -> DigestOutputStream(outputStream, messageDigest).use { digestOutputStream -> inputStream.copyTo(digestOutputStream) } } } return messageDigest.digest().toHex() } return null } private fun backupPartitions(context: Context, destination: ExtendedFile): Partitions? { val partitions = HashMap() for (partitionName in PartitionUtil.PartitionNames) { if (_backupPartitions[partitionName] == true) { val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) if (blockDevice != null) { addMessage("Saving $partitionName") val hash = backupPartition(blockDevice, destination.getChildFile("$partitionName.img")) if (hash != null) { partitions[partitionName] = hash } } } } if (partitions.isNotEmpty()) { return Partitions.from(partitions) } return null } private fun createBackupDir(context: Context, now: String): ExtendedFile { @SuppressLint("SdCardPath") val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher") if (!externalDir.exists()) { if (!externalDir.mkdir()) { log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true) } } val backupsDir = externalDir.getChildFile("backups") if (!backupsDir.exists()) { if (!backupsDir.mkdir()) { log(context, "Failed to create backups dir", shouldThrow = true) } } val backupDir = backupsDir.getChildFile(now) if (backupDir.exists()) { log(context, "Backup $now already exists", shouldThrow = true) } else { if (!backupDir.mkdir()) { log(context, "Failed to create backup dir", shouldThrow = true) } } return backupDir } @OptIn(ExperimentalSerializationApi::class) fun backup(context: Context) { launch { _clearFlash() val currentKernelVersion = _slotInfo.value.bootImgInfo.kernelVersion ?: run { _getKernel(context) _slotInfo.value.bootImgInfo.kernelVersion ?: System.getProperty("os.version")!! } val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) val backupDir = createBackupDir(context, now) addMessage("Saving backup $now") val hashes = backupPartitions(context, backupDir) if (hashes == null) { log(context, "No partitions saved", shouldThrow = true) } val jsonFile = backupDir.getChildFile("backup.json") val backup = Backup(now, "raw", currentKernelVersion, sha1, null, hashes, hashAlgorithm) val indentedJson = Json { prettyPrint = true } jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } _backups[now] = backup addMessage("Backup $now saved") _wasFlashSuccess.value = true SharedViewModels.mainViewModel.markRefreshNeeded() } } @OptIn(ExperimentalSerializationApi::class) fun backupZip(context: Context, callback: () -> Unit) { launch { val source = context.contentResolver.openInputStream(flashUri!!) if (source != null) { _getKernel(context) val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) val backupDir = createBackupDir(context, now) val jsonFile = backupDir.getChildFile("backup.json") val backup = Backup(now, "ak3", _slotInfo.value.bootImgInfo.kernelVersion!!, null, flashFilename) val indentedJson = Json { prettyPrint = true } jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } val destination = backupDir.getChildFile(flashFilename!!) source.use { inputStream -> destination.outputStream().use { outputStream -> inputStream.copyTo(outputStream) } } _backups[now] = backup withContext (Dispatchers.Main) { callback.invoke() } } else { log(context, "AK3 zip is missing", shouldThrow = true) } SharedViewModels.mainViewModel.markRefreshNeeded() } } private fun resetSlot() { val activeSlotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] val newSlot = if (activeSlotSuffix == "_a") "_b" else "_a" Shell.cmd("resetprop -n ro.boot.slot_suffix $newSlot").exec() wasSlotReset = !wasSlotReset } @Suppress("FunctionName") private suspend fun _checkZip(context: Context, zip: File, callback: (() -> Unit)? = null) { if (zip.exists()) { try { @Suppress("BlockingMethodInNonBlockingContext") val zipFile = ZipFile(zip) zipFile.use { z -> if (z.getEntry("anykernel.sh") == null) { log(context, "Invalid AK3 zip", shouldThrow = true) } withContext (Dispatchers.Main) { callback?.invoke() } } } catch (e: Exception) { zip.delete() throw e } } else { log(context, "Failed to save zip", shouldThrow = true) } } @Suppress("FunctionName") private fun _copyFile(context: Context, currentBackup: String, filename: String) { flashUri = null flashFilename = filename @SuppressLint("SdCardPath") val externalDir = File("/sdcard/KernelFlasher") val backupsDir = fileSystemManager.getFile("$externalDir/backups") val backupDir = backupsDir.getChildFile(currentBackup) if (!backupDir.exists()) { log(context, "Backup $currentBackup does not exists", shouldThrow = true) } val source = backupDir.getChildFile(flashFilename!!) val zip = File(context.filesDir, flashFilename!!) source.newInputStream().use { inputStream -> zip.outputStream().use { outputStream -> inputStream.copyTo(outputStream) } } } @Suppress("FunctionName") private fun _copyFile(context: Context, uri: Uri) { flashUri = uri flashFilename = if (uri.scheme == "file") { File(uri.path ?: "").name } else { context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> if (!cursor.moveToFirst()) return@use null val name = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) return@use cursor.getString(name) } ?: "ak3.zip" } val source = context.contentResolver.openInputStream(uri) val file = File(context.filesDir, flashFilename!!) source.use { inputStream -> file.outputStream().use { outputStream -> inputStream?.copyTo(outputStream) } } } @Suppress("FunctionName") private fun _copyDriver(context: Context, uri: Uri) { flashUri = uri flashFilename = context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> if (!cursor.moveToFirst()) return@use null val name = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) return@use cursor.getString(name) } ?: "kernelsu.ko" val source = context.contentResolver.openInputStream(uri) val file = File(context.filesDir, "kernelsu.ko") source.use { inputStream -> file.outputStream().use { outputStream -> inputStream?.copyTo(outputStream) } } Shell.cmd("chmod +rwx $file").exec() } @OptIn(ExperimentalSerializationApi::class) @Suppress("FunctionName") private suspend fun _flashAk3(context: Context, type: String) { if (!isActive) { resetSlot() } val zip = File(context.filesDir.canonicalPath, flashFilename!!) _checkZip(context, zip) try { if (zip.exists()) { _wasFlashSuccess.value = false val files = File(context.filesDir.canonicalPath) val flashScript = File(files, "flash_ak3$type.sh") val result = Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER).build().newJob().add("F=$files Z=\"$zip\" /system/bin/sh $flashScript").to(flashOutput, flashOutput).exec() val outputTail = flashOutput.takeLast(5).joinToString("\n") val fakeFail = "sched_setattr: not found" in outputTail && "Done!" in outputTail && result.code == 127 if (result.isSuccess || fakeFail) { log(context, "AnyKernel Zip flashed successfully") _wasFlashSuccess.value = true } else { log(context, "Failed to flash zip", shouldThrow = false) // Log.e(TAG, "Error: ${result.stderr.joinToString("\n")}") } clearTmp(context) } else { log(context, "AK3 zip is missing", shouldThrow = true) } } catch (e: Exception) { clearFlash(context) throw e } finally { uiPrint("") if (wasSlotReset) { resetSlot() viewModelScope.launch(Dispatchers.Main) { showCautionDialog() // Show dialog instead of uiPrint } } SharedViewModels.mainViewModel.markRefreshNeeded() } } fun switchSlot(context: Context) { viewModelScope.launch(Dispatchers.IO) { try { // Get current slot val currentSlot = Shell.cmd("getprop ro.boot.slot_suffix").exec().out.firstOrNull() ?: "_a" val targetSlot = if (currentSlot == "_a") "b" else "a" // Execute bootctl command val bootctl = File(context.filesDir, "bootctl") val result = Shell.cmd("$bootctl set-active-boot-slot $targetSlot").exec() if (result.isSuccess) { log(context, "Slot was successfully switched to $targetSlot", shouldThrow = false) } else { log(context, "Failed to switch slot", shouldThrow = false) } } catch (e: Exception) { withContext(Dispatchers.Main) { Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show() } throw e } } } fun flashAk3(context: Context, currentBackup: String, filename: String) { launch { _clearFlash() _copyFile(context, currentBackup, filename) _flashAk3(context, "") } } fun flashAk3(context: Context, uri: Uri) { launch { _clearFlash() _copyFile(context, uri) _flashAk3(context, "") } } fun flashAk3_mkbootfs(context: Context, currentBackup: String, filename: String) { launch { _clearFlash() _copyFile(context, currentBackup, filename) _flashAk3(context, "_mkbootfs") } } fun flashAk3_mkbootfs(context: Context, uri: Uri) { launch { _clearFlash() _copyFile(context, uri) _flashAk3(context, "_mkbootfs") } } @OptIn(ExperimentalSerializationApi::class) fun flashKsuDriver(context: Context, uri: Uri) { launch { _clearFlash() addMessage("Copying KernelSU Driver ...") _copyDriver(context, uri) if (!isActive) { resetSlot() } val driver = fileSystemManager.getFile(context.filesDir, "kernelsu.ko") val newBootImg = File(context.filesDir, "new-boot.img") var image: ExtendedFile? = null try { if (driver.exists()) { addMessage("Copied $flashFilename") _wasFlashSuccess.value = false val partitionName = _slotInfo.value.ramdiskInfo.ramdiskLocation?.removeSuffix(".img") ?: "boot" val magiskboot = File(context.filesDir, "magiskboot") val ksuinit = File(context.filesDir, "ksuinit") addMessage("Unpacking $partitionName") var ramdisk = File(context.filesDir, "ramdisk.cpio") if(partitionName == "boot") Shell.cmd("$magiskboot unpack $boot").exec() else if(partitionName == "init_boot") Shell.cmd("$magiskboot unpack $initBoot").exec() else { var vendor_boot = PartitionUtil.findPartitionBlockDevice(context, "vendor_boot", slotSuffix) Shell.cmd("$magiskboot unpack $vendor_boot").exec() ramdisk = File(context.filesDir, "vendor_ramdisk/ramdisk.cpio") if (!ramdisk.exists()) ramdisk = File(context.filesDir, "vendor_ramdisk/init_boot.cpio") } if (ramdisk.exists()) { addMessage("Patching Ramdisk") if(Shell.cmd("$magiskboot cpio $ramdisk 'exists kernelsu.ko'").to(flashOutput, flashOutput).exec().isSuccess) Shell.cmd("$magiskboot cpio $ramdisk 'rm init' 'add 0755 init $ksuinit' 'rm kernelsu.ko' 'add 0755 kernelsu.ko $driver'").to(flashOutput, flashOutput).exec() else Shell.cmd("$magiskboot cpio $ramdisk 'mv init init.real' 'add 0755 init $ksuinit' 'add 0755 kernelsu.ko $driver'").to(flashOutput, flashOutput).exec() addMessage("Repacking $partitionName") if(partitionName == "boot") Shell.cmd("$magiskboot repack $boot").exec() else if(partitionName == "init_boot") Shell.cmd("$magiskboot repack $initBoot").exec() else { var vendor_boot = PartitionUtil.findPartitionBlockDevice(context, "vendor_boot", slotSuffix) Shell.cmd("$magiskboot repack $vendor_boot").exec() } if(newBootImg.exists()) { image = fileSystemManager.getFile(context.filesDir, "new-boot.img") } else { Shell.cmd("$magiskboot cleanup").exec() log(context, "Image Repack Failed!", shouldThrow = true) } } else { Shell.cmd("$magiskboot cleanup").exec() log(context, "Ramdisk not found", shouldThrow = true) } Shell.cmd("$magiskboot cleanup").exec() addMessage("Flashing $image to $partitionName$slotSuffix ...") val blockDevice = partitionName.let { PartitionUtil.findPartitionBlockDevice(context, it, slotSuffix) } if (blockDevice != null && blockDevice.exists()) { if (PartitionUtil.isPartitionLogical(context, partitionName)) { if (image != null) { PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message -> addMessage(message) } } } else { if (image != null) { PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) } } } else { log(context, "Partition $partitionName$slotSuffix was not found", shouldThrow = true) } addMessage("Flashed ${image?.name} to $partitionName$slotSuffix") addMessage("Cleaning up ...") clearTmp(context) addMessage("Done.") _wasFlashSuccess.value = true } else { log(context, "KernelSU Driver is missing", shouldThrow = true) } } catch (e: Exception) { clearFlash(context) throw e } finally { addMessage("") if (driver.exists()) driver.delete() if (newBootImg.exists()) newBootImg.delete() if (wasSlotReset) { resetSlot() viewModelScope.launch(Dispatchers.Main) { showCautionDialog() // Show dialog instead of uiPrint } } } SharedViewModels.mainViewModel.markRefreshNeeded() } } @OptIn(ExperimentalSerializationApi::class) fun flashImage(context: Context, uri: Uri, partitionName: String) { launch { _clearFlash() addMessage("Copying image ...") _copyFile(context, uri) if (!isActive) { resetSlot() } val image = fileSystemManager.getFile(context.filesDir, flashFilename!!) try { if (image.exists()) { addMessage("Copied $flashFilename") _wasFlashSuccess.value = false addMessage("Flashing $flashFilename to $partitionName ...") val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) if (blockDevice != null && blockDevice.exists()) { if (PartitionUtil.isPartitionLogical(context, partitionName)) { PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message -> addMessage(message) } } else { PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) } } else { log(context, "Partition $partitionName was not found", shouldThrow = true) } addMessage("Flashed $flashFilename to $partitionName") addMessage("Cleaning up ...") clearTmp(context) addMessage("Done.") _wasFlashSuccess.value = true } else { log(context, "Partition image is missing", shouldThrow = true) } } catch (e: Exception) { clearFlash(context) throw e } finally { addMessage("") if (wasSlotReset) { resetSlot() viewModelScope.launch(Dispatchers.Main) { showCautionDialog() // Show dialog instead of uiPrint } } SharedViewModels.mainViewModel.markRefreshNeeded() } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.updates import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import kotlinx.serialization.ExperimentalSerializationApi @Suppress("UnusedReceiverParameter") @ExperimentalMaterial3Api @ExperimentalSerializationApi @Composable fun ColumnScope.UpdatesAddContent( viewModel: UpdatesViewModel, navController: NavController ) { @Suppress("UNUSED_VARIABLE") val context = LocalContext.current var url by remember { mutableStateOf("") } OutlinedTextField( value = url, onValueChange = { url = it }, label = { Text(stringResource(R.string.url)) }, modifier = Modifier .fillMaxWidth() ) Spacer(Modifier.height(5.dp)) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.add(url) { navController.navigate("updates/view/$it") { popUpTo("updates") } } } ) { Text(stringResource(R.string.add)) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.updates import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnitType import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.ui.components.DataCard import kotlinx.serialization.ExperimentalSerializationApi @Suppress("UnusedReceiverParameter") @ExperimentalMaterial3Api @ExperimentalSerializationApi @ExperimentalUnitApi @Composable fun ColumnScope.UpdatesChangelogContent( viewModel: UpdatesViewModel, @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController ) { viewModel.currentUpdate?.let { currentUpdate -> DataCard(currentUpdate.kernelName) Spacer(Modifier.height(16.dp)) Text(viewModel.changelog!!, style = LocalTextStyle.current.copy( fontFamily = FontFamily.Monospace, fontSize = TextUnit(12.0f, TextUnitType.Sp), lineHeight = TextUnit(18.0f, TextUnitType.Sp) ) ) } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.updates import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.ViewButton import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalMaterial3Api @ExperimentalSerializationApi @Composable fun ColumnScope.UpdatesContent( viewModel: UpdatesViewModel, navController: NavController ) { @Suppress("UNUSED_VARIABLE") val context = LocalContext.current DataCard(stringResource(R.string.updates)) if (viewModel.updates.isNotEmpty()) { for (update in viewModel.updates.sortedByDescending { it.kernelDate }) { Spacer(Modifier.height(16.dp)) DataCard( title = update.kernelName, button = { AnimatedVisibility(!viewModel.isRefreshing) { Column { ViewButton(onClick = { navController.navigate("updates/view/${update.id}") }) } } } ) { val cardWidth = remember { mutableIntStateOf(0) } DataRow(stringResource(R.string.version), update.kernelVersion, mutableMaxWidth = cardWidth) DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(update.kernelDate), mutableMaxWidth = cardWidth) DataRow( label = stringResource(R.string.last_updated), value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!), labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), labelStyle = MaterialTheme.typography.labelMedium.copy( fontStyle = FontStyle.Italic ), valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), valueStyle = MaterialTheme.typography.titleSmall.copy( fontStyle = FontStyle.Italic ), mutableMaxWidth = cardWidth ) } } } AnimatedVisibility(!viewModel.isRefreshing) { Column { Spacer(Modifier.height(12.dp)) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { navController.navigate("updates/add") } ) { Text(stringResource(R.string.add)) } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesUrlState.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.updates @Suppress("unused") class UpdatesUrlState { // TODO: validate the url field } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.updates import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalMaterial3Api @ExperimentalSerializationApi @Composable fun ColumnScope.UpdatesViewContent( viewModel: UpdatesViewModel, navController: NavController ) { val context = LocalContext.current viewModel.currentUpdate?.let { currentUpdate -> DataCard(currentUpdate.kernelName) { val cardWidth = remember { mutableIntStateOf(0) } DataRow(stringResource(R.string.version), currentUpdate.kernelVersion, mutableMaxWidth = cardWidth) DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(currentUpdate.kernelDate), mutableMaxWidth = cardWidth) DataRow( label = stringResource(R.string.last_updated), value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!), labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), labelStyle = MaterialTheme.typography.labelMedium.copy( fontStyle = FontStyle.Italic ), valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), valueStyle = MaterialTheme.typography.titleSmall.copy( fontStyle = FontStyle.Italic, ), mutableMaxWidth = cardWidth ) } AnimatedVisibility(!viewModel.isRefreshing) { Column { Spacer(Modifier.height(5.dp)) OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } } ) { Text(stringResource(R.string.changelog)) } // TODO: add download progress indicator OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.downloadKernel(context) } ) { Text(stringResource(R.string.download)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.update() } ) { Text(stringResource(R.string.check_for_updates)) } OutlinedButton( modifier = Modifier .fillMaxWidth(), shape = RoundedCornerShape(4.dp), onClick = { viewModel.delete { navController.popBackStack() } } ) { Text(stringResource(R.string.delete)) } } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewModel.kt ================================================ package com.github.capntrips.kernelflasher.ui.screens.updates import android.content.ContentValues import android.content.Context import android.net.Uri import android.os.Environment import android.provider.MediaStore import android.util.Log import android.widget.Toast import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavController import androidx.room.Room import com.github.capntrips.kernelflasher.common.types.room.AppDatabase import com.github.capntrips.kernelflasher.common.types.room.updates.Update import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateSerializer import com.topjohnwu.superuser.nio.FileSystemManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.OkHttpClient import okhttp3.Request import java.io.IOException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import kotlin.io.path.Path import kotlin.io.path.name @ExperimentalSerializationApi class UpdatesViewModel( context: Context, @Suppress("unused") private val fileSystemManager: FileSystemManager, private val navController: NavController, private val _isRefreshing: MutableState ) : ViewModel() { companion object { const val TAG: String = "KernelFlasher/UpdatesState" val lastUpdatedFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) } private val client = OkHttpClient() private val db = Room.databaseBuilder(context, AppDatabase::class.java, "kernel-flasher").build() private val updateDao = db.updateDao() private val _updates: SnapshotStateList = mutableStateListOf() var currentUpdate: Update? = null var changelog: String? = null val updates: List get() = _updates val isRefreshing: Boolean get() = _isRefreshing.value init { launch { val updates = updateDao.getAll() viewModelScope.launch(Dispatchers.Main) { _updates.addAll(updates) } } } private fun launch(block: suspend () -> Unit) { viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.Main) { _isRefreshing.value = true } try { block() } catch (e: Exception) { withContext (Dispatchers.Main) { Log.e(TAG, e.message, e) navController.navigate("error/${e.message}") { popUpTo("main") } } } viewModelScope.launch(Dispatchers.Main) { _isRefreshing.value = false } } } @Suppress("SameParameterValue") private fun log(context: Context, message: String, shouldThrow: Boolean = false) { Log.d(TAG, message) if (!shouldThrow) { viewModelScope.launch(Dispatchers.Main) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } else { throw Exception(message) } } fun clearCurrent() { currentUpdate = null changelog = null } fun add(url: String, callback: (updateId: Int) -> Unit) { launch { val request = Request.Builder() .url(url) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected response: $response") val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string()) update.updateUri = url update.lastUpdated = Date() val updateId = updateDao.insert(update).toInt() val inserted = updateDao.load(updateId) withContext (Dispatchers.Main) { _updates.add(inserted) callback.invoke(updateId) } } } } fun update() { launch { val request = Request.Builder() .url(currentUpdate!!.updateUri!!) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected response: $response") val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string()) currentUpdate!!.let { withContext (Dispatchers.Main) { it.kernelName = update.kernelName it.kernelVersion = update.kernelVersion it.kernelLink = update.kernelLink it.kernelChangelogUrl = update.kernelChangelogUrl it.kernelDate = update.kernelDate it.kernelSha1 = update.kernelSha1 it.supportLink = update.supportLink it.lastUpdated = Date() viewModelScope.launch(Dispatchers.IO) { updateDao.update(it) } } } } } } fun downloadChangelog(callback: () -> Unit) { launch { val request = Request.Builder() .url(currentUpdate!!.kernelChangelogUrl) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected response: $response") changelog = response.body!!.string() withContext (Dispatchers.Main) { callback.invoke() } } } } private fun insertDownload(context: Context, filename: String): Uri? { val resolver = context.contentResolver val values = ContentValues() values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename) values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip") values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) return resolver.insert(MediaStore.Files.getContentUri("external"), values) } fun downloadKernel(context: Context) { launch { val remoteUri = Uri.parse(currentUpdate!!.kernelLink) val filename = Path(remoteUri.path!!).name val localUri = insertDownload(context, filename) localUri!!.let { uri -> val request = Request.Builder() .url(remoteUri.toString()) .build() client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected response: $response") response.body!!.byteStream().use { inputStream -> context.contentResolver.openOutputStream(uri)!!.use { outputStream -> inputStream.copyTo(outputStream) } } log(context, "Saved $filename to Downloads") } } } } fun delete(callback: () -> Unit) { launch { updateDao.delete(currentUpdate!!) withContext (Dispatchers.Main) { _updates.remove(currentUpdate!!) callback.invoke() currentUpdate = null } } } } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt ================================================ package com.github.capntrips.kernelflasher.ui.theme import androidx.compose.ui.graphics.Color val Orange500 = Color(0xFFFF9800) ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt ================================================ package com.github.capntrips.kernelflasher.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext @Composable fun KernelFlasherTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, content: @Composable () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } darkTheme -> darkColorScheme() else -> lightColorScheme() } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) } ================================================ FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Type.kt ================================================ package com.github.capntrips.kernelflasher.ui.theme import androidx.compose.material3.Typography val Typography = Typography().copy() ================================================ FILE: app/src/main/res/drawable/ic_launcher_background.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_splash_animation.xml ================================================ ================================================ FILE: app/src/main/res/drawable/ic_splash_foreground.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml ================================================ ================================================ FILE: app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml ================================================ ================================================ FILE: app/src/main/res/resources.properties ================================================ unqualifiedResLocale=en-US ================================================ FILE: app/src/main/res/values/colors.xml ================================================ #000000 ================================================ FILE: app/src/main/res/values/strings.xml ================================================ Kernel Flasher Root is required Root service disconnected Device Model Build Number Kernel Name Kernel Version Slot Suffix Slot Slot A Slot B Boot SHA1 Vendor DLKM Exists Not Found Mounted Unmounted View Backups Save ramoops Save dmesg Save logcat Back Backup Updates Flash Flash using mkbootfs Flash AK3 Zip Flash AK3 Zip using mkbootfs Flash Partition Image Flash KernelSU LKM Driver Restore Check Kernel Version Mount Vendor DLKM Unmount Vendor DLKM Map Vendor DLKM Unmap Vendor DLKM Migrate No backups found Delete Add URL Version Date Released Last Updated Changelog Check for Updates Download Reboot Soft Reboot Reboot to Recovery Reboot to Bootloader Reboot to Download Reboot to EDL Save AK3 Log Save Flash Log Save Backup Log Save Restore Log Save AK3 Zip as Backup Backup Type Hashes Partition selection unavailable for legacy backups boot.img Format init_boot.img Format Ramdisk Format SUSFS Version Backup Selected Partitions vendor_boot.img Format Active Unbootable Successful ================================================ FILE: app/src/main/res/values/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-it/strings.xml ================================================ Kernel Flasher È richiesta il root Servizio root disconnesso Dispositivo Modello Numero build Nome kernel Versione kernel Suffisso slot Slot Slot A Slot B Boot SHA1 Vendor DLKM Esiste Non trovato Montato Smontato Visualizza Backups Salva ramoops Salva dmesg Salva logcat Indietro Backup Aggiornamenti Flash Flash utilizzando mkbootfs Flash AK3 Zip Flash AK3 Zip utilizzando mkbootfs Flash Partitione Image Flash KernelSU LKM Driver Ripristina Controlla versione kernel Monta Vendor DLKM Smonta Vendor DLKM Mappa Vendor DLKM Rimuovi mappa Vendor DLKM Migrare Nessun backup trovato Elimina Aggiungi URL Versione Data di rilascio Ultimo aggiornamento Changelog Controlla gli aggiornamenti Download Riavvio Riavvio Veloce Riavvio in Recovery Riavvio in Bootloader Riavvio in Download Riavvio in EDL Salva AK3 Log Salva Flash Log Salva Backup Log Salva Restore Log Salva AK3 Zip come backup Tipo Backup Hashes Selezione della partizione non disponibile per il backup legacy Formato boot.img Formato init_boot.img Formato Ramdisk Versione SUSFS Backup Partizioni Selezionate Formato vendor_boot.img Attivo Non avviabile Riuscito ================================================ FILE: app/src/main/res/values-ja/strings.xml ================================================  Kernel Flasher Root 権限が必要です Root サービスが切断されました デバイス モデル ビルド番号 カーネル名 カーネルバージョン スロットの接頭辞 スロット スロット A スロット B Boot SHA1 Vendor DLKM あり なし マウント済み アンマウント済み 表示 バックアップ ramoops を保存 dmesg を保存 logcat を保存 戻る バックアップ 更新 フラッシュ mkbootfs を使用してフラッシュ AK3 Zip をフラッシュ mkbootfs を使用して AK3 Zip をフラッシュ パーティションイメージをフラッシュ KernelSU LKM ドライバをフラッシュ 復元 カーネルバージョンを確認 Vendor DLKM をマウント Vendor DLKM をアンマウント Vendor DLKM をマップ Vendor DLKM をアンマップ 移行 バックアップが見つかりません 削除 追加 URL バージョン リリース日 最終更新 更新履歴 更新を確認 ダウンロード 再起動 ソフトリブート リカバリーで再起動 ブートローダーで再起動 ダウンロードモードで再起動 EDL で再起動 AK3 ログを保存 フラッシュログを保存 バックアップログを保存 復元ログを保存 AK3 Zip をバックアップとして保存 バックアップタイプ ハッシュ レガシーバックアップではパーティションを選択できません boot.img 形式 init_boot.img 形式 Ramdisk 形式 SUSFS バージョン 選択したパーティションをバックアップする vendor_boot.img 形式 アクティブ 起動不可 成功 ================================================ FILE: app/src/main/res/values-night/themes.xml ================================================ ================================================ FILE: app/src/main/res/values-pl/strings.xml ================================================ Kernel Flasher Wymagane uprawnienia Superużytkownika Usługa Superużytkownika została odłączona Urządzenie Model Kompilacja Nazwa jądra Wersja jądra Przyrostek slotu Slot Slot A Slot B Boot SHA1 Vendor DLKM Istnieje Nie znaleziono Zamontowany Niezamontowany Wybierz Kopie zapasowe Zapisz ramoops Zapisz dmesg Zapisz logcat Wstecz Utwórz kopię zapasową Aktualizacje Sflashuj Sflashuj przy użyciu mkbootfs Sflashuj archiwum AK3 Flashuj archiwum AK3 za pomocą mkbootfs Sflashuj obraz partycji Sflashuj KernelSU LKM Driver Przywróć Sprawdź wersję jądra Zamontuj Vendor DLKM Odmontuj Vendor DLKM Migruj Zmapuj Vendor DLKM Odmapuj Vendor DLKM Nie znaleziono kopii zapasowych Usuń Dodaj Adres URL Wersja Data publikacji Ostatnia aktualizacja Lista zmian Sprawdź aktualizacje Pobierz Uruchom ponownie Miękki restart Uruchom ponownie do trybu Recovery Uruchom ponownie do trybu Bootloader Uruchom ponownie do trybu Download Uruchom ponownie do trybu EDL Zapisz dziennik AK3 Zapisz dziennik Flashowania Zapisz dziennik kopii zapasowej Zapisz dziennik przywracania Zapisz archiwum AK3 jako kopię zapasową Typ kopii zapasowej Sumy kontrolne Wybór partycji niedostępny dla kopii zapasowych starszego formatu Format boot.img Format init_boot.img Format Ramdisk Wersja SUSFS Utwórz kopię zapasową wybranych partycji Format vendor_boot.img Aktywny ================================================ FILE: app/src/main/res/values-pt-rBR/strings.xml ================================================ Kernel Flasher Root é necessário Serviço root desconectado Dispositivo Modelo Número da versão Nome do kernel Versão do kernel Sufixo de slot Slot A Slot B Boot SHA1 Vendor DLKM Existe Não encontrado Montado Desmontado Visualizar Backups Salvar ramoops Salvar dmesg Salvar logcat Voltar Backup Atualizações Flash Flash using mkbootfs Flash AK3 ZIP Flash AK3 Zip using mkbootfs Flashar imagem de partição Flash KernelSU LKM Driver Restaurar Verificar versão do kernel Montar Vendor DLKM Desmontar Vendor DLKM Mapear Vendor DLKM Desmapear Vendor DLKM Migrar Nenhum backup encontrado Excluir Adicionar URL Versão Data de lançamento Ultima atualização Registro de alterações Verificar por atualizações Baixar Reiniciar Reinicialização suave Reiniciar em modo Recovery Reiniciar em modo Bootloader Reiniciar em modo Download Reiniciar em modo EDL Salvar registro do AK3 Salvar registro do flash Salvar registro do backup Salvar registro da restauração Salvar AK3 ZIP como backup Tipo de backup Hashes Seleção de partição indisponível para backups antigos Formato do boot.img Formato do init_boot.img Formato do Ramdisk SUSFS Versão Fazer backup das partições selecionadas Formato do vendor_boot.img Ativo ================================================ FILE: app/src/main/res/values-ru/strings.xml ================================================ Kernel Flasher Требуется root-доступ Служба root отключена Устройство Модель Номер сборки Kernel Name Версия ядра Slot Suffix Слот Слот A Слот B Boot SHA1 Vendor DLKM Существует Не найдено Подключено Отключено Просмотр Резервные копии Сохранить ramoops Сохранить dmesg Сохранить logcat Назад Резервное копирование Обновления Прошивка Прошить помощью mkbootfs Прошить AK3 Zip Прошить AK3 Zip с помощью mkbootfs Прошить образ раздела Прошивка KernelSU LKM Driver Восстановить Проверить версию ядра Подключить Vendor DLKM Отключить Vendor DLKM Сопоставить Vendor DLKM Отменить сопоставление Vendor DLKM Мигрировать Резервные копии не найдены Удалить Добавить URL Версия Дата выпуска Последнее обновление Список изменений Проверить обновления Скачать Перезагрузить Soft Reboot Reboot to Recovery Reboot to Bootloader Reboot to Download Mode Reboot to EDL Сохранить журнал AK3 Сохранить журнал прошивки Сохранить журнал резервного копирования Сохранить журнал восстановления Сохранить AK3 Zip как резервную копию Тип резервной копии Хеши Выбор раздела недоступен для устаревших резервных копий boot.img формат init_boot.img формат Ramdisk формат SUSFS Версия Резервное копирование выбранных разделов vendor_boot.img формат Активный ================================================ FILE: app/src/main/res/values-zh-rCN/strings.xml ================================================ Kernel Flasher 需要 Root 权限 Root 服务已断开 设备 型号 构建版本 内核名 内核版本 插槽后缀 插槽 A 插槽 B Boot 哈希 Vendor DLKM 存在 未找到 已挂载 未卸载 查看 备份 保存 ramoops 保存 dmesg 保存 logcat 返回 备份 更新 刷入 使用 mkbootfs 刷入 刷入 AK3 压缩包 刷入 AK3 压缩包 使用 mkbootfs 刷入分区镜像 刷入 KernelSU LKM Driver 恢复 检查内核版本 挂载 Vendor DLKM 卸载 Vendor DLKM 映射 Vendor DLKM 取消映射 Vendor DLKM 迁移 没有找到备份 删除 添加 链接地址 版本 发布日期 更新日期 变更日志 检查更新 下载 重启 软重启 重启到 Recovery 重启到 Bootloader 重启到 Download 重启到 EDL 保存 AK3 日志 保存刷写日志 保存备份日志 保存恢复日志 将 AK3 包作为备份保存 备份类型 哈希值 旧的备份无法选择分区 boot.img 格式 init_boot.img 格式 Ramdisk 格式 SUSFS 版本 備份选定的分区 vendor_boot.img 格式 活跃 ================================================ FILE: app/src/main/res/values-zh-rTW/strings.xml ================================================ Kernel Flasher 需要 Root 授權 Root 服務已斷開 裝置 型號 構建版本 核心名 核心版本 插槽字尾 插槽 A 插槽 B Boot 雜湊 Vendor DLKM 存在 未找到 已掛載 未解除安裝 檢視 備份 儲存 ramoops 儲存 dmesg 儲存 logcat 返回 備份 更新 刷入 使用 mkbootfs 刷入 刷入 AK3 壓縮包 刷入 AK3 压缩包 使用 mkbootfs 刷入分割槽映象 刷入 KernelSU LKM Driver 還原 檢查核心版本 掛載 Vendor DLKM 解除安裝 Vendor DLKM 對映 Vendor DLKM 取消對映 Vendor DLKM 遷移 沒有找到備份 刪除 新增 連結地址 版本 釋出日期 更新日期 變更日誌 檢查更新 下載 重啟 軟重啟 重啟到 Recovery 重啟到 Bootloader 重啟到 Download 重啟到 EDL 儲存 AK3 日誌 儲存刷寫日誌 儲存備份日誌 儲存還原日誌 將 AK3 包作為備份儲存 備份型別 雜湊值 舊的備份無法選擇分割槽 boot.img 格式 init_boot.img 格式 Ramdisk 格式 SUSFS 版本 備份选定的分区 vendor_boot.img 格式 活躍 ================================================ FILE: app/src/main/res/xml/backup_rules.xml ================================================ ================================================ FILE: app/src/main/res/xml/data_extraction_rules.xml ================================================ ================================================ FILE: build.gradle.kts ================================================ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.devtools.ksp) apply false alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.serialization) apply false // alias(libs.plugins.compose.compiler) apply false } tasks.register("clean", Delete::class) { delete(rootProject.layout.buildDirectory) } ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] kotlin = "2.3.0" androidx-activity-compose = "1.12.2" androidx-appcompat = "1.7.1" androidx-compose = "1.10.0" androidx-compose-material3 = "1.3.2" androidx-core-ktx = "1.17.0" androidx-core-splashscreen = "1.2.0" androidx-lifecycle = "2.10.0" androidx-navigation-compose = "2.9.6" androidx-room = "2.8.4" kotlinx-serialization-json = "1.9.0" libsu = "6.0.0" material = "1.13.0" okhttp = "5.3.2" android-application = "8.13.2" devtools-ksp = "2.3.4" retrofit = "3.0.0" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } androidx-compose-ui = { group = "androidx.compose.ui", name="ui", version.ref = "androidx-compose" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" } androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" } androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" } androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" } converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" } libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } [plugins] android-application = { id = "com.android.application", version.ref = "android-application" } devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Fri Apr 14 13:36:42 CDT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official # Enables namespacing of each library's R class so that its R class includes only the # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.defaults.buildfeatures.buildconfig=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh # # Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin or MSYS, switch paths to Windows format before running java if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=`expr $i + 1` done case $i in 0) set -- ;; 1) set -- "$args0" ;; 2) set -- "$args0" "$args1" ;; 3) set -- "$args0" "$args1" "$args2" ;; 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() maven { url = uri("https://jitpack.io") } } } rootProject.name = "Kernel Flasher" include(":app")