Full Code of fatalcoder524/KernelFlasher for AI

allow-errors bfa337727d54 cached
90 files
288.0 KB
68.2k tokens
1 requests
Download .txt
Showing preview only (320K chars total). Download the full file or copy to clipboard to get everything.
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 <phh@phh.me>

    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 <https://www.gnu.org/licenses/>.


================================================
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 <methods>;
}

# 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 <fields>;
}

# ============ 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 {
    <fields>;
}
-keepclassmembers class **.AppUpdater$GitHubAsset {
    <fields>;
}

# ============ 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.KernelFlasher"
        tools:targetApi="33">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/Theme.MainSplashScreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="content" android:mimeType="application/zip" />
                <data android:scheme="file" android:mimeType="application/zip" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:mimeType="application/zip" />
                <data android:scheme="file" />
                <data android:scheme="content" />
                <data android:host="*" />
                <data android:pathPattern=".*\\.zip" />
            </intent-filter>
        </activity>
        <service android:name=".FilesystemService" android:exported="false" tools:ignore="Instantiatable" />
    </application>

</manifest>

================================================
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 <RECOVERY_API_VERSION> <OUTFD> <ZIPFILE>
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 <RECOVERY_API_VERSION> <OUTFD> <ZIPFILE>
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<AppUpdater.GitHubRelease>
}

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<GitHubAsset>
    )

    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<String>, () -> 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<Uri>(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<String>()

    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<FstabEntry>(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<String, String>) = 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<Date> {
    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>(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<Update>

    @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<Int>? = 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<Uri?>(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<String>,
    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<Boolean>,
    private val _backups: MutableMap<String, Backup>
) : ViewModel() {
    companion object {
        const val TAG: String = "KernelFlasher/BackupsState"
    }

    private val _restoreOutput: SnapshotStateList<String> = 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<String, Boolean> = 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<Boolean> = mutableStateOf(false)

    val restoreOutput: List<String>
        get() = _restoreOutput
    val backupPartitions: MutableMap<String, Boolean>
        get() = _backupPartitions
    val isRefreshing: Boolean
        get() = _isRefreshing.value
    val backups: Map<String, Backup>
        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<String, String>()
        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<Boolean> = mutableStateOf(true)
    private val _isRefreshRequired = mutableStateOf(true)
    private var _error: String? = null
    private var _backups: MutableMap<String, Backup> = mutableMapOf()
    var showSlotIntentDialog: MutableState<Boolean> = mutableStateOf(false)

    var pendingFlashUri: Uri? = null
    var slotSuffixForFlash = mutableStateOf<String?>(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<String>,
        val onConfirm: () -> Unit
    )

    var updateDialogData by mutableStateOf<UpdateDialogData?>(null)
        private set

    fun showUpdateDialog(title: String, changelog: List<String>, 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<Boolean>
) : 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<Boolean>,
    val isActive: Boolean,
    val slotSuffix: String,
    val boot: File,
    val initBoot: File?,
    private val _backups: MutableMap<String, Backup>
) : 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<SlotInfo> = mutableStateOf(SlotInfo(BootSlotInfo(), BootImgInfo(), RamdiskInfo()))
    var hasVendorDlkm: Boolean = false
    var isVendorDlkmMapped: Boolean = false
    var isVendorDlkmMounted: Boolean = false
    private val _flashOutput: SnapshotStateList<String> = mutableStateListOf()
    private val _wasFlashSuccess: MutableState<Boolean?> = mutableStateOf(null)
    private val _backupPartitions: SnapshotStateMap<String, Boolean> = 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<Boolean> = mutableStateOf(false)
	private val _showConfirmDialog: MutableState<Boolean> = mutableStateOf(false)
    var flashActionType: String = ""
    var flashActionURI: Uri? = null
    var flashActionPartName: String? = null

    val sha1: String?
        get() = _sha1
    val flashOutput: List<String>
        get() = _flashOutput
    val uiPrintedOutput: List<String>
        get() = _flashOutput.filter { it.startsWith("ui_print") }.map { it.substringAfter("ui_print").trim() }.filter { it.isNotEmpty() || it == "" }
    val wasFlashSuccess: MutableState<Boolean?>
        get() = _wasFlashSuccess
    val backupPartitions: MutableMap<String, Boolean>
        get() = _backupPartitions
    val isRefreshing: MutableState<Boolean>
        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<String>()
        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<String>()
            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_bo
Download .txt
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
Condensed preview — 90 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (317K chars).
[
  {
    "path": ".github/dependabot.yml",
    "chars": 143,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"gradle\"\n    directory: \"/\"\n    target-branch: \"allow-errors\"\n    schedule:\n "
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 4445,
    "preview": "name: Android Build\npermissions:\n  contents: write\non:\n  workflow_dispatch:\n  push:\n  pull_request:\n\njobs:\n  build:\n    "
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 5298,
    "preview": "name: Android Release\npermissions:\n  contents: write\non:\n  workflow_dispatch:\n    inputs:\n      increment_major:\n       "
  },
  {
    "path": ".gitignore",
    "chars": 200,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/app/release\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.propertie"
  },
  {
    "path": "LICENSE",
    "chars": 2618,
    "preview": "Copyright (c) 2022 capntrips\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this softw"
  },
  {
    "path": "README.md",
    "chars": 636,
    "preview": "[![GitHub release](https://img.shields.io/github/release/fatalcoder524/KernelFlasher)](https://GitHub.com/fatalcoder524/"
  },
  {
    "path": "app/.gitignore",
    "chars": 41,
    "preview": "/build\r\n/release\r\nbuild.gradle.bak\r\n*.bak"
  },
  {
    "path": "app/build.gradle.kts",
    "chars": 3409,
    "preview": "plugins {\n    alias(libs.plugins.android.application)\n    alias(libs.plugins.devtools.ksp)\n    alias(libs.plugins.kotlin"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 2012,
    "preview": "# GENERAL\n-dontobfuscate\n-keepattributes Signature\n-keepattributes RuntimeVisibleAnnotations\n\n# ANDROID INTERFACES / AID"
  },
  {
    "path": "app/schemas/com.github.capntrips.kernelflasher.common.types.room.AppDatabase/1.json",
    "chars": 2490,
    "preview": "{\n  \"formatVersion\": 1,\n  \"database\": {\n    \"version\": 1,\n    \"identityHash\": \"bbe3033de836fa33fb2ed46b5272124e\",\n    \"e"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 2279,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl",
    "chars": 113,
    "preview": "package com.github.capntrips.kernelflasher;\n\ninterface IFilesystemService {\n    IBinder getFileSystemService();\n}"
  },
  {
    "path": "app/src/main/assets/flash_ak3.sh",
    "chars": 1157,
    "preview": "#!/system/bin/sh\n\n## setup for testing:\nunzip -p \"$Z\" tools*/busybox > $F/busybox_ak;\nunzip -p \"$Z\" META-INF/com/google/"
  },
  {
    "path": "app/src/main/assets/flash_ak3_mkbootfs.sh",
    "chars": 1237,
    "preview": "#!/system/bin/sh\n\n## setup for testing:\nunzip -p \"$Z\" tools*/busybox > $F/busybox_ak;\nunzip -p \"$Z\" META-INF/com/google/"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/AppUpdater.kt",
    "chars": 5427,
    "preview": "package com.github.capntrips.kernelflasher\r\n\r\nimport android.annotation.SuppressLint\r\nimport android.app.DownloadManager"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt",
    "chars": 514,
    "preview": "package com.github.capntrips.kernelflasher\n\nimport android.content.Intent\nimport android.os.IBinder\nimport com.topjohnwu"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/MainActivity.kt",
    "chars": 34776,
    "preview": "package com.github.capntrips.kernelflasher\n\nimport android.animation.ObjectAnimator\nimport android.animation.PropertyVal"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt",
    "chars": 161,
    "preview": "package com.github.capntrips.kernelflasher\n\ninternal class MainListener(private val callback: () -> Unit) {\n    fun resu"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt",
    "chars": 8320,
    "preview": "package com.github.capntrips.kernelflasher.common\n\nimport android.content.Context\nimport com.github.capntrips.kernelflas"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt",
    "chars": 195,
    "preview": "package com.github.capntrips.kernelflasher.common.extensions\n\nimport kotlin.ByteArray\n\nobject ByteArray {\n    fun ByteAr"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt",
    "chars": 894,
    "preview": "package com.github.capntrips.kernelflasher.common.extensions\n\nimport com.topjohnwu.superuser.nio.ExtendedFile\nimport jav"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt",
    "chars": 437,
    "preview": "package com.github.capntrips.kernelflasher.common.types.backups\n\nimport com.github.capntrips.kernelflasher.common.types."
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt",
    "chars": 183,
    "preview": "package com.github.capntrips.kernelflasher.common.types.partitions\n\nimport kotlinx.serialization.Serializable\n\n@Serializ"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt",
    "chars": 418,
    "preview": "package com.github.capntrips.kernelflasher.common.types.partitions\n\nimport kotlinx.serialization.Serializable\n\n@Serializ"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt",
    "chars": 1662,
    "preview": "package com.github.capntrips.kernelflasher.common.types.partitions\n\nimport kotlinx.serialization.Serializable\n\n@Serializ"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt",
    "chars": 490,
    "preview": "package com.github.capntrips.kernelflasher.common.types.room\n\nimport androidx.room.Database\nimport androidx.room.RoomDat"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt",
    "chars": 348,
    "preview": "package com.github.capntrips.kernelflasher.common.types.room\n\nimport androidx.room.TypeConverter\nimport java.util.Date\n\n"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt",
    "chars": 3581,
    "preview": "package com.github.capntrips.kernelflasher.common.types.room.updates\n\nimport androidx.room.ColumnInfo\nimport androidx.ro"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt",
    "chars": 522,
    "preview": "package com.github.capntrips.kernelflasher.common.types.room.updates\n\nimport androidx.room.Dao\nimport androidx.room.Dele"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt",
    "chars": 1567,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.BorderStroke\nimport android"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt",
    "chars": 1520,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport a"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt",
    "chars": 1870,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.Row\nimport androidx."
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt",
    "chars": 869,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.Column\nimport androi"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataValue.kt",
    "chars": 1511,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.c"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt",
    "chars": 1119,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt",
    "chars": 2867,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport android.net.Uri\nimport android.provider.OpenableColumns"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt",
    "chars": 4954,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.animation.core.animateFloatAsState\nimp"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt",
    "chars": 4814,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.animation.AnimatedVisibility\nimport an"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt",
    "chars": 1106,
    "preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt",
    "chars": 4895,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens\n\nimport androidx.compose.animation.AnimatedVisibility\nimport andro"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt",
    "chars": 6571,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.backups\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt",
    "chars": 12335,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.backups\n\nimport android.annotation.SuppressLint\nimport android.con"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt",
    "chars": 13102,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.backups\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt",
    "chars": 1876,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.error\n\nimport androidx.compose.foundation.layout.Box\nimport androi"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt",
    "chars": 4551,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.main\n\nimport android.os.Build\nimport androidx.compose.animation.An"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt",
    "chars": 8488,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.main\n\nimport android.annotation.SuppressLint\nimport android.conten"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt",
    "chars": 2064,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.reboot\n\nimport android.os.Build\nimport android.os.PowerManager\nimp"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt",
    "chars": 2236,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.reboot\n\nimport android.content.Context\nimport android.util.Log\nimp"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt",
    "chars": 5365,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.slot\n\nimport androidx.compose.animation.AnimatedVisibility\nimport "
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt",
    "chars": 15768,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.slot\n\nimport android.provider.OpenableColumns\nimport androidx.acti"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt",
    "chars": 37634,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.slot\n\nimport android.annotation.SuppressLint\nimport android.conten"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt",
    "chars": 1899,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.foundation.layout.ColumnScope\nimp"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt",
    "chars": 1526,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.foundation.layout.ColumnScope\nimp"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt",
    "chars": 3802,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesUrlState.kt",
    "chars": 144,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\n@Suppress(\"unused\")\nclass UpdatesUrlState {\n    // TODO: "
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt",
    "chars": 4283,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewModel.kt",
    "chars": 7833,
    "preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport android.content.ContentValues\nimport android.conte"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt",
    "chars": 130,
    "preview": "package com.github.capntrips.kernelflasher.ui.theme\n\nimport androidx.compose.ui.graphics.Color\n\nval Orange500 = Color(0x"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt",
    "chars": 1106,
    "preview": "package com.github.capntrips.kernelflasher.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystem"
  },
  {
    "path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Type.kt",
    "chars": 135,
    "preview": "package com.github.capntrips.kernelflasher.ui.theme\n\nimport androidx.compose.material3.Typography\n\nval Typography = Typo"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 330,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
    "chars": 900,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "app/src/main/res/drawable/ic_splash_animation.xml",
    "chars": 1175,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
  },
  {
    "path": "app/src/main/res/drawable/ic_splash_foreground.xml",
    "chars": 1623,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:wi"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 344,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 344,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/resources.properties",
    "chars": 26,
    "preview": "unqualifiedResLocale=en-US"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 118,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_splash_background\">#000000</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 4163,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"app_name\">Kernel Flasher</string>\n    <stri"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "chars": 802,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Theme.KernelFlasher\" parent=\"Theme.Material3.DayNigh"
  },
  {
    "path": "app/src/main/res/values-it/strings.xml",
    "chars": 4296,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"app_name\">Kernel Flasher</string>\n    <stri"
  },
  {
    "path": "app/src/main/res/values-ja/strings.xml",
    "chars": 3705,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n  <string name=\"app_n"
  },
  {
    "path": "app/src/main/res/values-night/themes.xml",
    "chars": 400,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"Theme.KernelFlasher\" parent=\"Theme.Material3.DayNigh"
  },
  {
    "path": "app/src/main/res/values-pl/strings.xml",
    "chars": 4223,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">Kernel Flasher</string>\n    <string name="
  },
  {
    "path": "app/src/main/res/values-pt-rBR/strings.xml",
    "chars": 4132,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <string name=\"app_name\">Kernel Flasher</string>\r\n    <string na"
  },
  {
    "path": "app/src/main/res/values-ru/strings.xml",
    "chars": 4220,
    "preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <string name=\"app_name\">Kernel Flasher</string>\n    <stri"
  },
  {
    "path": "app/src/main/res/values-zh-rCN/strings.xml",
    "chars": 3412,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">Kernel Flasher</string>\n    <string name="
  },
  {
    "path": "app/src/main/res/values-zh-rTW/strings.xml",
    "chars": 3418,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\">Kernel Flasher</string>\n    <string name="
  },
  {
    "path": "app/src/main/res/xml/backup_rules.xml",
    "chars": 478,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample backup rules file; uncomment and customize as necessary.\n   See htt"
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "chars": 551,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n "
  },
  {
    "path": "build.gradle.kts",
    "chars": 370,
    "preview": "plugins {\n    alias(libs.plugins.android.application) apply false\n    alias(libs.plugins.devtools.ksp) apply false\n    a"
  },
  {
    "path": "gradle/libs.versions.toml",
    "chars": 3420,
    "preview": "[versions]\nkotlin = \"2.3.0\"\n\nandroidx-activity-compose = \"1.12.2\"\nandroidx-appcompat = \"1.7.1\"\nandroidx-compose = \"1.10."
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 233,
    "preview": "#Fri Apr 14 13:36:42 CDT 2023\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://"
  },
  {
    "path": "gradle.properties",
    "chars": 1407,
    "preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
  },
  {
    "path": "gradlew",
    "chars": 5766,
    "preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
  },
  {
    "path": "gradlew.bat",
    "chars": 2674,
    "preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
  },
  {
    "path": "settings.gradle.kts",
    "chars": 401,
    "preview": "pluginManagement {\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n    }\n}\ndepen"
  }
]

// ... and 3 more files (download for full content)

About this extraction

This page contains the full source code of the fatalcoder524/KernelFlasher GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 90 files (288.0 KB), approximately 68.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!