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