Showing preview only (320K chars total). Download the full file or copy to clipboard to get everything.
Repository: fatalcoder524/KernelFlasher
Branch: allow-errors
Commit: bfa337727d54
Files: 90
Total size: 288.0 KB
Directory structure:
gitextract_gnutdixd/
├── .github/
│ ├── dependabot.yml
│ └── workflows/
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── .gitignore
│ ├── build.gradle.kts
│ ├── proguard-rules.pro
│ ├── schemas/
│ │ └── com.github.capntrips.kernelflasher.common.types.room.AppDatabase/
│ │ └── 1.json
│ └── src/
│ └── main/
│ ├── AndroidManifest.xml
│ ├── aidl/
│ │ └── com/
│ │ └── github/
│ │ └── capntrips/
│ │ └── kernelflasher/
│ │ └── IFilesystemService.aidl
│ ├── assets/
│ │ ├── flash_ak3.sh
│ │ ├── flash_ak3_mkbootfs.sh
│ │ ├── ksuinit
│ │ └── mkbootfs
│ ├── java/
│ │ └── com/
│ │ └── github/
│ │ └── capntrips/
│ │ └── kernelflasher/
│ │ ├── AppUpdater.kt
│ │ ├── FilesystemService.kt
│ │ ├── MainActivity.kt
│ │ ├── MainListener.kt
│ │ ├── common/
│ │ │ ├── PartitionUtil.kt
│ │ │ ├── extensions/
│ │ │ │ ├── ByteArray.kt
│ │ │ │ └── ExtendedFile.kt
│ │ │ └── types/
│ │ │ ├── backups/
│ │ │ │ └── Backup.kt
│ │ │ ├── partitions/
│ │ │ │ ├── FsMgrFlags.kt
│ │ │ │ ├── FstabEntry.kt
│ │ │ │ └── Partitions.kt
│ │ │ └── room/
│ │ │ ├── AppDatabase.kt
│ │ │ ├── Converters.kt
│ │ │ └── updates/
│ │ │ ├── Update.kt
│ │ │ └── UpdateDao.kt
│ │ └── ui/
│ │ ├── components/
│ │ │ ├── Card.kt
│ │ │ ├── DataCard.kt
│ │ │ ├── DataRow.kt
│ │ │ ├── DataSet.kt
│ │ │ ├── DataValue.kt
│ │ │ ├── DialogButton.kt
│ │ │ ├── FlashButton.kt
│ │ │ ├── FlashList.kt
│ │ │ ├── SlotCard.kt
│ │ │ └── ViewButton.kt
│ │ ├── screens/
│ │ │ ├── RefreshableScreen.kt
│ │ │ ├── backups/
│ │ │ │ ├── BackupsContent.kt
│ │ │ │ ├── BackupsViewModel.kt
│ │ │ │ └── SlotBackupsContent.kt
│ │ │ ├── error/
│ │ │ │ └── ErrorScreen.kt
│ │ │ ├── main/
│ │ │ │ ├── MainContent.kt
│ │ │ │ └── MainViewModel.kt
│ │ │ ├── reboot/
│ │ │ │ ├── RebootContent.kt
│ │ │ │ └── RebootViewModel.kt
│ │ │ ├── slot/
│ │ │ │ ├── SlotContent.kt
│ │ │ │ ├── SlotFlashContent.kt
│ │ │ │ └── SlotViewModel.kt
│ │ │ └── updates/
│ │ │ ├── UpdatesAddContent.kt
│ │ │ ├── UpdatesChangelogContent.kt
│ │ │ ├── UpdatesContent.kt
│ │ │ ├── UpdatesUrlState.kt
│ │ │ ├── UpdatesViewContent.kt
│ │ │ └── UpdatesViewModel.kt
│ │ └── theme/
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── res/
│ ├── drawable/
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_launcher_foreground.xml
│ │ ├── ic_splash_animation.xml
│ │ └── ic_splash_foreground.xml
│ ├── mipmap-anydpi-v26/
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ ├── resources.properties
│ ├── values/
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ ├── values-it/
│ │ └── strings.xml
│ ├── values-ja/
│ │ └── strings.xml
│ ├── values-night/
│ │ └── themes.xml
│ ├── values-pl/
│ │ └── strings.xml
│ ├── values-pt-rBR/
│ │ └── strings.xml
│ ├── values-ru/
│ │ └── strings.xml
│ ├── values-zh-rCN/
│ │ └── strings.xml
│ ├── values-zh-rTW/
│ │ └── strings.xml
│ └── xml/
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── build.gradle.kts
├── gradle/
│ ├── libs.versions.toml
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/"
target-branch: "allow-errors"
schedule:
interval: "daily"
================================================
FILE: .github/workflows/build.yml
================================================
name: Android Build
permissions:
contents: write
on:
workflow_dispatch:
push:
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Important: get all tags!
- name: Get Latest Tag
run: |
latest_tag=$(git describe --tags --abbrev=0)
echo "LATEST_TAG=${latest_tag}" >> $GITHUB_ENV
- name: Count Commits Since Latest Tag
run: |
# Get the number of commits since the latest tag
commits_since_tag=$(git rev-list ${LATEST_TAG}..HEAD --count)
echo "COMMITS_SINCE_TAG=${commits_since_tag}" >> $GITHUB_ENV
- name: Generate Version
run: |
# Extract version components
version_base="${LATEST_TAG#v}"
major=$(echo $version_base | cut -d. -f1)
minor=$(echo $version_base | cut -d. -f2)
patch=$(echo $version_base | cut -d. -f3)
# Increment patch by the number of commits since the last tag
new_patch=$((patch + $COMMITS_SINCE_TAG))
# Construct the new version string
new_version="$major.$minor.$new_patch"
# Calculate the version code (e.g., 1.0.5 -> 10005)
version_code=$((major * 10000 + minor * 100 + new_patch))
# Set environment variables
echo "NEW_VERSION_NAME=${new_version}" >> $GITHUB_ENV
echo "NEW_VERSION_CODE=${version_code}" >> $GITHUB_ENV
- name: Update build.gradle.kts with new version code and version name
run: |
# Update versionCode and versionName in build.gradle
sed -i "s/versionCode [0-9]\+/versionCode ${NEW_VERSION_CODE}/" app/build.gradle
sed -i "s/versionName \"[^\"]*\"/versionName \"${NEW_VERSION_NAME}\"/" app/build.gradle
- name: Print Version
run: |
echo "Version Name: $NEW_VERSION_NAME"
echo "Version Code: $NEW_VERSION_CODE"
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew assembleRelease
tree app/build/outputs/apk/release
- uses: r0adkll/sign-android-release@v1.0.4
name: Sign app APK
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
id: sign_app
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "35.0.0"
- name: Rename APK
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
run: |
ls -al app/build/outputs/apk/release
echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}"
cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk
- name: Rename APK
if: github.repository != github.event.pull_request.head.repo.full_name && github.event_name == 'pull_request'
run: |
ls -al app/build/outputs/apk/release
cp ./app/build/outputs/apk/release/app-release-unsigned.apk KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk
- name: Upload APK
uses: actions/upload-artifact@v4.3.5
with:
name: KernelFlasher_${{ env.NEW_VERSION_CODE }}
path: KernelFlasher_${{ env.NEW_VERSION_CODE }}.apk
# - name: Rename apk
# run: |
# ls -al
# DATE=$(date +'%y.%m.%d')
# echo "TAG=$DATE" >> $GITHUB_ENV
# - name: Upload release
# uses: ncipollo/release-action@v1.14.0
# with:
# allowUpdates: true
# removeArtifacts: true
# name: "1.${{ github.run_number }}.0"
# tag: "v1.${{ github.run_number }}.0"
# body: |
# Note: QMod KernelFlasher, support ksu-lkm
# artifacts: "*.apk"
================================================
FILE: .github/workflows/publish.yml
================================================
name: Android Release
permissions:
contents: write
on:
workflow_dispatch:
inputs:
increment_major:
description: 'Increment Major Version by?'
required: true
default: 0
type: number
increment_minor:
description: 'Increment Minor Version by?'
required: true
default: 0
type: number
increment_patch:
description: 'Increment Patch Version by?'
required: true
default: 0
type: number
changes_in_release:
description: 'Changes in release'
required: true
default: 'Minor changes'
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Important: get all tags!
- name: Get Latest Tag
run: |
latest_tag=$(git describe --tags --abbrev=0)
echo "LATEST_TAG=${latest_tag}" >> $GITHUB_ENV
- name: Generate Version
run: |
# Extract version components
version_base="${LATEST_TAG#v}"
major=$(echo $version_base | cut -d. -f1)
minor=$(echo $version_base | cut -d. -f2)
patch=$(echo $version_base | cut -d. -f3)
# Increment versions by the specified input values
new_major=$((major + ${{ github.event.inputs.increment_major }}))
if [ ${{ github.event.inputs.increment_major }} -gt 0 ]; then
new_minor="${{ github.event.inputs.increment_minor }}"
new_patch="${{ github.event.inputs.increment_patch }}"
else
new_minor=$((minor + ${{ github.event.inputs.increment_minor }}))
if [ ${{ github.event.inputs.increment_minor }} -gt 0 ]; then
new_patch="${{ github.event.inputs.increment_patch }}"
else
new_patch=$((patch + ${{ github.event.inputs.increment_patch }}))
fi
fi
# Construct the new version string
new_version="$new_major.$new_minor.$new_patch"
# Calculate the version code (e.g., 1.0.5 -> 10005)
version_code=$((new_major * 10000 + new_minor * 100 + new_patch))
# Set environment variables
echo "NEW_VERSION_NAME=${new_version}" >> $GITHUB_ENV
echo "NEW_VERSION_CODE=${version_code}" >> $GITHUB_ENV
- name: Update build.gradle.kts with new version code and version name
run: |
# Update versionCode and versionName in build.gradle
sed -i "s/versionCode [0-9]\+/versionCode ${NEW_VERSION_CODE}/" app/build.gradle
sed -i "s/versionName \"[^\"]*\"/versionName \"${NEW_VERSION_NAME}\"/" app/build.gradle
- name: Print Version
run: |
echo "Version Name: $NEW_VERSION_NAME"
echo "Version Code: $NEW_VERSION_CODE"
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
run: |
chmod +x ./gradlew
./gradlew assembleRelease
tree app/build/outputs/apk/release
- uses: qlenlen/sign-android-release@v2.0.1
name: Sign app APK
id: sign_app
with:
releaseDirectory: app/build/outputs/apk/release
signingKeyBase64: ${{ secrets.KEYSTORE }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: "35.0.0"
- name: Commit the changes
run: |
# Configure git user
git config user.name "github-actions"
git config user.email "github-actions@users.noreply.github.com"
# Add modified build.gradle file
git add app/build.gradle
# Commit the changes
git commit -m "Update versionName and versionCode to ${NEW_VERSION_NAME} and ${NEW_VERSION_CODE}"
# Push changes to the current branch
git push https://github-actions:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} HEAD:${GITHUB_REF#refs/heads/}
- name: Rename APK
run: |
ls -al app/build/outputs/apk/release
echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}"
cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher_${{ env.NEW_VERSION_NAME }}.apk
- name: Upload APK
uses: actions/upload-artifact@v4.3.5
with:
name: KernelFlasher_${{ env.NEW_VERSION_NAME }}
path: KernelFlasher_${{ env.NEW_VERSION_NAME }}.apk
- name: Upload release
uses: ncipollo/release-action@v1.14.0
with:
allowUpdates: true
removeArtifacts: true
draft: true
name: ${{ env.NEW_VERSION_NAME }}
tag: "v${{ env.NEW_VERSION_NAME }}"
body: |
Note: KernelFlasher + allow-errors
Changes in this Release:
${{ github.event.inputs.changes_in_release }}
artifacts: "*.apk"
================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/app/release
/build
/captures
.externalNativeBuild
.cxx
local.properties
.secrets
.env
.github/workflows/build_local.yml
KernelFlasher.apk
/app/build/*
================================================
FILE: LICENSE
================================================
Copyright (c) 2022 capntrips
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This project bundles lptools (https://github.com/phhusson/vendor_lptools),
which is licensed under the Apache 2.0 license:
Copyright (C) 2020 Pierre-Hugues Husson <phh@phh.me>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
This project bundles magiskboot (https://github.com/topjohnwu/Magisk),
which is licensed under the GPLv3+ license:
Copyright (C) 2017-2022 John Wu <@topjohnwu>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
================================================
FILE: README.md
================================================
[](https://GitHub.com/fatalcoder524/KernelFlasher/releases/)
[](https://GitHub.com/fatalcoder524/KernelFlasher/releases/)
# Kernel Flasher
Kernel Flasher is an Android app to flash, backup, and restore kernels.
## Usage
`View` a slot and choose to `Flash` an AK3 zip, `Backup` the kernel related partitions, or `Restore` a previous backup.
There are also options to toggle the mount and map status of `vendor_dlkm` and to save `dmesg` and `logcat`.
================================================
FILE: app/.gitignore
================================================
/build
/release
build.gradle.bak
*.bak
================================================
FILE: app/build.gradle.kts
================================================
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.devtools.ksp)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose.compiler)
}
android {
compileSdk = 36
namespace = "com.github.capntrips.kernelflasher"
defaultConfig {
applicationId = "com.github.capntrips.kernelflasher"
minSdk = 29
targetSdk = 36
versionCode = 10600
versionName = "1.6.0"
javaCompileOptions {
annotationProcessorOptions {
arguments += mapOf(
"room.schemaLocation" to "$projectDir/schemas",
"room.incremental" to "true",
)
}
}
ndk {
//noinspection ChromeOsAbiSupport
abiFilters.add("arm64-v8a")
}
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isMinifyEnabled = false
isShrinkResources = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
}
sourceSets {
getByName("main") {
jniLibs.srcDirs("src/main/jniLibs")
}
}
buildFeatures {
buildConfig = true
aidl = true
compose = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
jvmToolchain(21)
}
packaging {
resources {
excludes += setOf("/META-INF/{AL2.0,LGPL2.1}")
}
jniLibs {
useLegacyPackaging = true
}
dex {
useLegacyPackaging = true
}
}
androidResources {
generateLocaleConfig = true
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
arg("room.incremental", "true")
}
}
dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.room.runtime)
annotationProcessor(libs.androidx.room.compiler)
ksp(libs.androidx.room.compiler)
implementation(libs.libsu.core)
implementation(libs.libsu.io)
implementation(libs.libsu.nio)
implementation(libs.libsu.service)
implementation(libs.material)
implementation(libs.okhttp)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.serialization.json)
implementation(libs.retrofit)
implementation(libs.converter.gson)
}
================================================
FILE: app/proguard-rules.pro
================================================
# GENERAL
-dontobfuscate
-keepattributes Signature
-keepattributes RuntimeVisibleAnnotations
# ANDROID INTERFACES / AIDL
-keep class com.github.capntrips.kernelflasher.FilesystemService
-keep class * implements android.os.IInterface
-keepclassmembers class * {
public static ** asInterface(android.os.IBinder);
}
# Prevent R8 from stripping native methods used via JNI
-keepclasseswithmembernames class * {
native <methods>;
}
# Keep RootShell library (libsu)
-keep class com.topjohnwu.superuser.** { *; }
# RETROFIT2
-keep interface com.github.capntrips.kernelflasher.GitHubApi { *; }
-keep class retrofit2.** { *; }
-dontwarn retrofit2.**
# ============ GSON ============
-keep class com.google.gson.** { *; }
-keep class com.github.capntrips.kernelflasher.AppUpdater$* { *; }
# Keep all fields/methods annotated with Gson's @SerializedName
-keepclassmembers class * {
@com.google.gson.annotations.SerializedName <fields>;
}
# ============ KOTLIN ============
-keep class kotlin.Metadata { *; }
-keepattributes RuntimeVisibleAnnotations,RuntimeInvisibleAnnotations,RuntimeVisibleParameterAnnotations,RuntimeInvisibleParameterAnnotations,Exceptions,InnerClasses,EnclosingMethod,Signature,SourceFile,LineNumberTable,*Annotation*,Deprecated,SourceDir,CompilationID,LocalVariableTable,LocalVariableTypeTable,Module*
-keepclassmembers class **.AppUpdater$GitHubRelease {
<fields>;
}
-keepclassmembers class **.AppUpdater$GitHubAsset {
<fields>;
}
# ============ COROUTINES ============
-keepclassmembers class kotlinx.coroutines.BuildConfig { public static final boolean DEBUG; }
-keep class kotlinx.** { *; }
-dontwarn kotlinx.**
# ============ DOWNLOAD MANAGER & BROADCAST RECEIVER ============
-keep class com.github.capntrips.kernelflasher.AppUpdater { *; }
-keepclassmembers class com.github.capntrips.kernelflasher.AppUpdater { *; }
# Keep VectorDrawableCompat to avoid crashes or inflation errors
-keep class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat { *; }
================================================
FILE: app/schemas/com.github.capntrips.kernelflasher.common.types.room.AppDatabase/1.json
================================================
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "bbe3033de836fa33fb2ed46b5272124e",
"entities": [
{
"tableName": "Update",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER, `update_uri` TEXT, `kernel_name` TEXT NOT NULL, `kernel_version` TEXT NOT NULL, `kernel_link` TEXT NOT NULL, `kernel_changelog_url` TEXT NOT NULL, `kernel_date` INTEGER NOT NULL, `kernel_sha1` TEXT NOT NULL, `support_link` TEXT, `last_updated` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
},
{
"fieldPath": "updateUri",
"columnName": "update_uri",
"affinity": "TEXT"
},
{
"fieldPath": "kernelName",
"columnName": "kernel_name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kernelVersion",
"columnName": "kernel_version",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kernelLink",
"columnName": "kernel_link",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kernelChangelogUrl",
"columnName": "kernel_changelog_url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "kernelDate",
"columnName": "kernel_date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "kernelSha1",
"columnName": "kernel_sha1",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "supportLink",
"columnName": "support_link",
"affinity": "TEXT"
},
{
"fieldPath": "lastUpdated",
"columnName": "last_updated",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bbe3033de836fa33fb2ed46b5272124e')"
]
}
}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.KernelFlasher"
tools:targetApi="33">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/Theme.MainSplashScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" android:mimeType="application/zip" />
<data android:scheme="file" android:mimeType="application/zip" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="application/zip" />
<data android:scheme="file" />
<data android:scheme="content" />
<data android:host="*" />
<data android:pathPattern=".*\\.zip" />
</intent-filter>
</activity>
<service android:name=".FilesystemService" android:exported="false" tools:ignore="Instantiatable" />
</application>
</manifest>
================================================
FILE: app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl
================================================
package com.github.capntrips.kernelflasher;
interface IFilesystemService {
IBinder getFileSystemService();
}
================================================
FILE: app/src/main/assets/flash_ak3.sh
================================================
#!/system/bin/sh
## setup for testing:
unzip -p "$Z" tools*/busybox > $F/busybox_ak;
unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary;
##
chmod 755 $F/busybox_ak;
$F/busybox_ak >/dev/null 2>&1
if [ $? -eq 0 ]; then
mv $F/busybox $F/busybox_orig
mv $F/busybox_ak $F/busybox
fi
$F/busybox chmod 755 $F/update-binary;
$F/busybox chown root:root $F/busybox $F/update-binary;
TMP=$F/tmp;
$F/busybox umount $TMP 2>/dev/null;
$F/busybox rm -rf $TMP 2>/dev/null;
$F/busybox mkdir -p $TMP;
$F/busybox mount -t tmpfs -o noatime tmpfs $TMP;
#$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1;
PATTERN='\$[Bb][Bb] chmod -R 755 tools bin;';
sed -i "/$PATTERN/i cp -f \"\$F/busybox\" \$AKHOME/tools;" "$F/update-binary";
# update-binary <RECOVERY_API_VERSION> <OUTFD> <ZIPFILE>
AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z";
RC=$?;
$F/busybox umount $TMP;
$F/busybox rm -rf $TMP;
$F/busybox mount -o ro,remount -t auto /;
$F/busybox rm -f $F/update-binary $F/busybox;
mv $F/busybox_orig $F/busybox
# work around libsu not cleanly accepting return or exit as last line
safereturn() { return $RC; }
safereturn;
================================================
FILE: app/src/main/assets/flash_ak3_mkbootfs.sh
================================================
#!/system/bin/sh
## setup for testing:
unzip -p "$Z" tools*/busybox > $F/busybox_ak;
unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary;
##
chmod 755 $F/busybox_ak;
$F/busybox_ak >/dev/null 2>&1
if [ $? -eq 0 ]; then
mv $F/busybox $F/busybox_orig
mv $F/busybox_ak $F/busybox
fi
$F/busybox chmod 755 $F/update-binary;
$F/busybox chown root:root $F/busybox $F/update-binary;
TMP=$F/tmp;
$F/busybox umount $TMP 2>/dev/null;
$F/busybox rm -rf $TMP 2>/dev/null;
$F/busybox mkdir -p $TMP;
$F/busybox mount -t tmpfs -o noatime tmpfs $TMP;
#$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1;
PATTERN='\$[Bb][Bb] chmod -R 755 tools bin;';
sed -i "/$PATTERN/i cp -f \"\$F/mkbootfs\" \$AKHOME/tools;" "$F/update-binary";
sed -i "/$PATTERN/i cp -f \"\$F/busybox\" \$AKHOME/tools;" "$F/update-binary";
# update-binary <RECOVERY_API_VERSION> <OUTFD> <ZIPFILE>
AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z";
RC=$?;
$F/busybox umount $TMP;
$F/busybox rm -rf $TMP;
$F/busybox mount -o ro,remount -t auto /;
$F/busybox rm -f $F/update-binary $F/busybox;
mv $F/busybox_orig $F/busybox
# work around libsu not cleanly accepting return or exit as last line
safereturn() { return $RC; }
safereturn;
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/AppUpdater.kt
================================================
package com.github.capntrips.kernelflasher
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Environment
import android.widget.Toast
import com.google.gson.Gson
import com.google.gson.annotations.SerializedName
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface GitHubApi {
@GET("repos/fatalcoder524/KernelFlasher/releases/latest")
suspend fun getLatestRelease(): Response<AppUpdater.GitHubRelease>
}
object AppUpdater {
data class GitHubAsset(
val name: String,
@SerializedName("browser_download_url") val downloadUrl: String
)
data class GitHubRelease(
@SerializedName("tag_name") val tagName: String,
val body: String,
val assets: List<GitHubAsset>
)
private val api: GitHubApi = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create(Gson()))
.build()
.create(GitHubApi::class.java)
// Compares version strings (e.g., v1.0.0 vs. v1.0.1)
private fun isNewer(latest: String, current: String): Boolean {
val latestParts = latest.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
val currentParts = current.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
return latestParts.zip(currentParts).any { (l, c) -> l > c }
}
suspend fun hasActiveInternetConnection(): Boolean = withContext(Dispatchers.IO) {
try {
val url = URL("https://connectivitycheck.gstatic.com/generate_204")
val connection = url.openConnection() as HttpURLConnection
connection.setRequestProperty("User-Agent", "Android")
connection.connectTimeout = 1500
connection.connect()
return@withContext connection.responseCode == 204
} catch (e: IOException) {
return@withContext false
}
}
// Checks if an update is available
suspend fun checkForUpdate(
context: Context,
currentVersion: String,
onShowDialog: (String, List<String>, () -> Unit) -> Unit
) {
val response = api.getLatestRelease()
if (response.isSuccessful) {
val release = response.body() ?: return
val latestVersion = release.tagName.removePrefix("v")
if (isNewer(latestVersion, currentVersion)) {
val apk = release.assets.find { it.name.endsWith(".apk") } ?: return
val dialogTitle = "New version: $latestVersion"
val dialogLines = listOf(
"Changelog:",
*release.body.split("\n").toTypedArray()
)
val confirmAction = { downloadAndInstallApk(context, apk.downloadUrl, latestVersion) }
onShowDialog(dialogTitle, dialogLines, confirmAction)
}
}
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private fun downloadAndInstallApk(context: Context, url: String, latestVersion: String) {
Toast.makeText(context, "Downloading Update in Background. Don't perform any operations till update is completed!", Toast.LENGTH_LONG).show()
val request = DownloadManager.Request(Uri.parse(url))
request.setTitle("Kernel Flasher Latest Download")
request.setDescription("Downloading update...")
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Kernel_Flasher_$latestVersion.apk")
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val id = manager.enqueue(request)
val receiver = object : BroadcastReceiver() {
override fun onReceive(c: Context?, intent: Intent?) {
val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (id == downloadId) {
val apkUri = manager.getUriForDownloadedFile(id)
val installIntent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(apkUri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(installIntent)
}
}
}
val appContext = context.applicationContext
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
appContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
} else {
@Suppress("DEPRECATION")
appContext.registerReceiver(receiver, intentFilter)
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt
================================================
package com.github.capntrips.kernelflasher
import android.content.Intent
import android.os.IBinder
import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager
class FilesystemService : RootService() {
inner class FilesystemIPC : IFilesystemService.Stub() {
override fun getFileSystemService(): IBinder {
return FileSystemManager.getService()
}
}
override fun onBind(intent: Intent): IBinder {
return FilesystemIPC()
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/MainActivity.kt
================================================
package com.github.capntrips.kernelflasher
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.provider.DocumentsContract
import android.util.Log
import android.view.View
import android.view.ViewTreeObserver
import android.view.Window
import android.view.animation.AccelerateInterpolator
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.TextButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.core.animation.doOnEnd
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.github.capntrips.kernelflasher.ui.components.DialogButton
import com.github.capntrips.kernelflasher.ui.screens.RefreshableScreen
import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsContent
import com.github.capntrips.kernelflasher.ui.screens.backups.SlotBackupsContent
import com.github.capntrips.kernelflasher.ui.screens.error.ErrorScreen
import com.github.capntrips.kernelflasher.ui.screens.main.MainContent
import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel
import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootContent
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotContent
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotFlashContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesAddContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesChangelogContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewContent
import com.github.capntrips.kernelflasher.ui.theme.KernelFlasherTheme
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.serialization.ExperimentalSerializationApi
import java.io.File
import kotlin.system.exitProcess
object SharedViewModels {
@OptIn(ExperimentalSerializationApi::class)
lateinit var mainViewModel: MainViewModel
}
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@ExperimentalMaterial3Api
@ExperimentalSerializationApi
@ExperimentalUnitApi
class MainActivity : ComponentActivity() {
companion object {
const val TAG: String = "MainActivity"
init {
Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER))
}
}
private var rootServiceConnected: Boolean = false
private var viewModel: MainViewModel? = null
private lateinit var mainListener: MainListener
var isAwaitingResult = false
inner class AidlConnection : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (!rootServiceConnected) {
val ipc: IFilesystemService = IFilesystemService.Stub.asInterface(service)
val binder: IBinder = ipc.fileSystemService
onAidlConnected(FileSystemManager.getRemote(binder))
rootServiceConnected = true
}
}
override fun onServiceDisconnected(name: ComponentName) {
setContent {
KernelFlasherTheme {
ErrorScreen(stringResource(R.string.root_service_disconnected))
}
}
}
}
private fun copyAsset(filename: String) {
val dest = File(filesDir, filename)
assets.open(filename).use { inputStream ->
dest.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
Shell.cmd("chmod +x $dest").exec()
}
private fun copyNativeBinary(filename: String) {
val binary = File(applicationInfo.nativeLibraryDir, "lib$filename.so")
println("binary: $binary")
val dest = File(filesDir, filename)
println("dest: $dest")
binary.inputStream().use { inputStream ->
dest.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
Shell.cmd("chmod +x $dest").exec()
}
override fun onCreate(savedInstanceState: Bundle?) {
requestWindowFeature(Window.FEATURE_NO_TITLE) // Hide the title bar
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val isZipIntent = intent?.action == Intent.ACTION_VIEW &&
(intent.type == "application/zip" || intent.data?.toString()?.endsWith(".zip") == true)
splashScreen.setOnExitAnimationListener { splashScreenView ->
val duration = if (isZipIntent) 100L else 250L
val scale = ObjectAnimator.ofPropertyValuesHolder(
splashScreenView.view,
PropertyValuesHolder.ofFloat(
View.SCALE_X,
1f,
0f
),
PropertyValuesHolder.ofFloat(
View.SCALE_Y,
1f,
0f
)
)
scale.interpolator = AccelerateInterpolator()
scale.duration = duration
scale.doOnEnd { splashScreenView.remove() }
scale.start()
}
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
return if (viewModel?.isRefreshing == false || Shell.isAppGrantedRoot() == false) {
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
false
}
}
}
)
Shell.getShell()
if (Shell.isAppGrantedRoot()!!) {
val intent = Intent(this, FilesystemService::class.java)
RootService.bind(intent, AidlConnection())
} else {
setContent {
KernelFlasherTheme {
ErrorScreen(stringResource(R.string.root_required))
}
}
}
}
@SuppressLint("WrongConstant")
private fun handleZipIntent(intent: Intent?) {
val action = intent?.action ?: return
val uri = when (action) {
Intent.ACTION_VIEW -> intent.data
Intent.ACTION_SEND -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
}
else -> null
} ?: return
if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) {
if(uri.scheme == "content" && DocumentsContract.isDocumentUri(this, uri)) {
val takeFlags =
intent.flags and (Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
try {
contentResolver.takePersistableUriPermission(uri, takeFlags)
} catch (se: SecurityException) {
Log.e(MainViewModel.Companion.TAG, se.message, se)
}
}
viewModel?.pendingFlashUri = uri
if(viewModel?.isAb == true)
viewModel?.showSlotIntentDialog?.value = true
else {
viewModel?.slotSuffixForFlash?.value = null
viewModel?.slotSuffixForFlash?.value = viewModel?.slotSuffix
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
if (Shell.isAppGrantedRoot() == true) {
handleZipIntent(intent)
if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) {
intent.replaceExtras(Bundle()) // Clear any existing data
setIntent(Intent()) // Replace with empty intent
}
}
}
fun onAidlConnected(fileSystemManager: FileSystemManager) {
try {
Shell.cmd("cd $filesDir").exec()
copyNativeBinary("lptools_static") // v20220825
copyNativeBinary("httools_static") // v3.2.0
copyNativeBinary("magiskboot") // v29.0
copyNativeBinary("bootctl") // aosp_arm64-img-13613025 android14
copyNativeBinary("busybox") // BusyBox v1.36.1.1
copyAsset("mkbootfs")
copyAsset("ksuinit")
copyAsset("flash_ak3.sh")
copyAsset("flash_ak3_mkbootfs.sh")
} catch (e: Exception) {
Log.e(TAG, e.message, e)
setContent {
KernelFlasherTheme {
ErrorScreen(e.message!!)
}
}
}
setContent {
val navController = rememberNavController()
viewModel = viewModel {
val application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY))
MainViewModel(application, fileSystemManager, navController)
}
val mainViewModel = viewModel!!
SharedViewModels.mainViewModel = mainViewModel
val slotSuffix by viewModel!!.slotSuffixForFlash
handleZipIntent(intent)
if (intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND) {
intent.replaceExtras(Bundle()) // Clear any existing data
setIntent(Intent()) // Replace with empty intent
}
val context = LocalContext.current
val dialogData = viewModel!!.updateDialogData
LaunchedEffect(Unit) {
if(AppUpdater.hasActiveInternetConnection()) {
AppUpdater.checkForUpdate(
context.applicationContext,
BuildConfig.VERSION_NAME
) { title, lines, confirm ->
viewModel!!.showUpdateDialog(title, lines, confirm)
}
}
val uri = viewModel?.pendingFlashUri
if (uri != null) {
if (viewModel?.isAb == true && slotSuffix == null) {
viewModel?.pendingFlashUri = uri
viewModel?.showSlotIntentDialog?.value = true
} else {
// Already have slot or not AB - flash directly
if (viewModel?.isAb == true && slotSuffix == "_b")
{
viewModel?.slotB?.flashActionType = "flashAk3"
viewModel?.slotB?.flashActionURI = uri
viewModel?.slotB?.showConfirmDialog()
}
else
{
viewModel?.slotA?.flashActionType = "flashAk3"
viewModel?.slotA?.flashActionURI = uri
viewModel?.slotA?.showConfirmDialog()
}
navController.navigate("slot${slotSuffix}")
navController.navigate("slot${slotSuffix}/flash") {
popUpTo("slot${slotSuffix}")
}
viewModel?.pendingFlashUri = null
viewModel?.slotSuffixForFlash?.value = null
}
}
}
var showExitDialog by remember { mutableStateOf(false) }
KernelFlasherTheme {
if (!mainViewModel.hasError) {
mainListener = MainListener {
mainViewModel.refresh(this)
}
val slotViewModelA = mainViewModel.slotA
val slotViewModelB = mainViewModel.slotB
val backupsViewModel = mainViewModel.backups
val updatesViewModel = mainViewModel.updates
val rebootViewModel = mainViewModel.reboot
BackHandler(enabled = !mainViewModel.isRefreshing, onBack = {})
// New back handler for exit
BackHandler(enabled = true) {
showExitDialog = true
}
val slotContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_a"
val slotViewModel = slotViewModelA
if (slotViewModel.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
slotViewModel.clearFlash(this@MainActivity)
}
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
SlotContent(slotViewModel, slotSuffix, navController)
}
}
val slotContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_b"
val slotViewModel = slotViewModelB
if (slotViewModel!!.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
slotViewModel.clearFlash(this@MainActivity)
}
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
SlotContent(slotViewModel, slotSuffix, navController)
}
}
val slotContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = ""
val slotViewModel = slotViewModelA
if (slotViewModel.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
slotViewModel.clearFlash(this@MainActivity)
}
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
SlotContent(slotViewModel, slotSuffix, navController)
}
}
val slotFlashContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_a"
val slotViewModel = slotViewModelA
RefreshableScreen(mainViewModel, navController) {
SlotFlashContent(slotViewModel, slotSuffix, navController)
}
}
val slotFlashContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_b"
val slotViewModel = slotViewModelB
RefreshableScreen(mainViewModel, navController) {
SlotFlashContent(slotViewModel!!, slotSuffix, navController)
}
}
val slotFlashContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = ""
val slotViewModel = slotViewModelA
RefreshableScreen(mainViewModel, navController) {
SlotFlashContent(slotViewModel, slotSuffix, navController)
}
}
val slotBackupsContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_a"
val slotViewModel = slotViewModelA
if (backStackEntry.arguments?.getString("backupId") != null) {
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
} else {
backupsViewModel.clearCurrent()
}
RefreshableScreen(mainViewModel, navController) {
SlotBackupsContent(slotViewModel, backupsViewModel, slotSuffix, navController)
}
}
val slotBackupsContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_b"
val slotViewModel = slotViewModelB
if (backStackEntry.arguments?.getString("backupId") != null) {
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
} else {
backupsViewModel.clearCurrent()
}
RefreshableScreen(mainViewModel, navController) {
SlotBackupsContent(slotViewModel!!, backupsViewModel, slotSuffix, navController)
}
}
val slotBackupsContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = ""
val slotViewModel = slotViewModelA
if (backStackEntry.arguments?.getString("backupId") != null) {
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
} else {
backupsViewModel.clearCurrent()
}
RefreshableScreen(mainViewModel, navController) {
SlotBackupsContent(slotViewModel, backupsViewModel, slotSuffix, navController)
}
}
val slotBackupFlashContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_a"
val slotViewModel = slotViewModelA
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
RefreshableScreen(mainViewModel, navController) {
SlotFlashContent(slotViewModel, slotSuffix, navController)
}
}
}
val slotBackupFlashContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = "_b"
val slotViewModel = slotViewModelB
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
RefreshableScreen(mainViewModel, navController) {
SlotFlashContent(slotViewModel!!, slotSuffix, navController)
}
}
}
val slotBackupFlashContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
val slotSuffix = ""
val slotViewModel = slotViewModelA
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
RefreshableScreen(mainViewModel, navController) {
SlotFlashContent(slotViewModel, slotSuffix, navController)
}
}
}
NavHost(navController = navController, startDestination = "main") {
composable("main") {
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
MainContent(mainViewModel, navController)
}
}
if (mainViewModel.isAb) {
composable("slot_a", content = slotContentA)
composable("slot_a/flash", content = slotFlashContentA)
composable("slot_a/flash/ak3", content = slotFlashContentA)
composable("slot_a/flash/image", content = slotFlashContentA)
composable("slot_a/flash/image/flash", content = slotFlashContentA)
composable("slot_a/backup", content = slotFlashContentA)
composable("slot_a/backup/backup", content = slotFlashContentA)
composable("slot_a/backups", content = slotBackupsContentA)
composable("slot_a/backups/{backupId}", content = slotBackupsContentA)
composable("slot_a/backups/{backupId}/restore", content = slotBackupsContentA)
composable("slot_a/backups/{backupId}/restore/restore", content = slotBackupsContentA)
composable("slot_a/backups/{backupId}/flash/ak3", content = slotBackupFlashContentA)
composable("slot_b", content = slotContentB)
composable("slot_b/flash", content = slotFlashContentB)
composable("slot_b/flash/ak3", content = slotFlashContentB)
composable("slot_b/flash/image", content = slotFlashContentB)
composable("slot_b/flash/image/flash", content = slotFlashContentB)
composable("slot_b/backup", content = slotFlashContentB)
composable("slot_b/backup/backup", content = slotFlashContentB)
composable("slot_b/backups", content = slotBackupsContentB)
composable("slot_b/backups/{backupId}", content = slotBackupsContentB)
composable("slot_b/backups/{backupId}/restore", content = slotBackupsContentB)
composable("slot_b/backups/{backupId}/restore/restore", content = slotBackupsContentB)
composable("slot_b/backups/{backupId}/flash/ak3", content = slotBackupFlashContentB)
} else {
composable("slot", content = slotContent)
composable("slot/flash", content = slotFlashContent)
composable("slot/flash/ak3", content = slotFlashContent)
composable("slot/flash/image", content = slotFlashContent)
composable("slot/flash/image/flash", content = slotFlashContent)
composable("slot/backup", content = slotFlashContent)
composable("slot/backup/backup", content = slotFlashContent)
composable("slot/backups", content = slotBackupsContent)
composable("slot/backups/{backupId}", content = slotBackupsContent)
composable("slot/backups/{backupId}/restore", content = slotBackupsContent)
composable("slot/backups/{backupId}/restore/restore", content = slotBackupsContent)
composable("slot/backups/{backupId}/flash/ak3", content = slotBackupFlashContent)
}
composable("backups") {
backupsViewModel.clearCurrent()
RefreshableScreen(mainViewModel, navController) {
BackupsContent(backupsViewModel, navController)
}
}
composable("backups/{backupId}") { backStackEntry ->
backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId")
if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
RefreshableScreen(mainViewModel, navController) {
BackupsContent(backupsViewModel, navController)
}
}
}
composable("updates") {
updatesViewModel.clearCurrent()
RefreshableScreen(mainViewModel, navController) {
UpdatesContent(updatesViewModel, navController)
}
}
composable("updates/add") {
RefreshableScreen(mainViewModel, navController) {
UpdatesAddContent(updatesViewModel, navController)
}
}
composable("updates/view/{updateId}") { backStackEntry ->
val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt()
val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId }
updatesViewModel.currentUpdate = currentUpdate
if (updatesViewModel.currentUpdate != null) {
// TODO: enable swipe refresh
RefreshableScreen(mainViewModel, navController) {
UpdatesViewContent(updatesViewModel, navController)
}
}
}
composable("updates/view/{updateId}/changelog") { backStackEntry ->
val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt()
val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId }
updatesViewModel.currentUpdate = currentUpdate
if (updatesViewModel.currentUpdate != null) {
RefreshableScreen(mainViewModel, navController) {
UpdatesChangelogContent(updatesViewModel, navController)
}
}
}
composable("reboot") {
RefreshableScreen(mainViewModel, navController) {
RebootContent(rebootViewModel, navController)
}
}
composable("error/{error}") { backStackEntry ->
val error = backStackEntry.arguments?.getString("error")
ErrorScreen(error!!)
}
}
} else {
ErrorScreen(mainViewModel.error)
}
if (dialogData != null) {
AlertDialog(
onDismissRequest = { viewModel!!.hideUpdateDialog() },
title = {
Text(
dialogData.title,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
},
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
dialogData.changelog.forEach {
Text(it, fontWeight = FontWeight.Bold)
}
}
},
confirmButton = {
DialogButton("Update APK") {
viewModel!!.hideUpdateDialog()
dialogData.onConfirm()
}
},
dismissButton = {
DialogButton("CANCEL") {
viewModel!!.hideUpdateDialog()
}
},
modifier = Modifier.padding(16.dp)
)
}
if (showExitDialog) {
AlertDialog(
onDismissRequest = { showExitDialog = false },
title = { Text("Exit App") },
text = { Text("Are you sure you want to exit?") },
confirmButton = {
TextButton(onClick = {
(context as? Activity)?.let {
it.finishAffinity()
exitProcess(0)
}
}) {
Text("Yes")
}
},
dismissButton = {
TextButton(onClick = { showExitDialog = false }) {
Text("No")
}
}
)
}
if (viewModel?.showSlotIntentDialog?.value == true) {
AlertDialog(
onDismissRequest = { viewModel?.showSlotIntentDialog?.value = false },
title = { Text("Select Slot to Flash") },
text = { Text("Choose the slot where the zip should be flashed.") },
confirmButton = {
TextButton(onClick = {
viewModel?.slotSuffixForFlash?.value = null
viewModel?.slotSuffixForFlash?.value = if(viewModel?.slotSuffix == "_a") "_b" else "_a"
viewModel?.showSlotIntentDialog?.value = false
}) {
Text("Inactive Slot")
}
},
dismissButton = {
TextButton(onClick = {
viewModel?.slotSuffixForFlash?.value = null
viewModel?.slotSuffixForFlash?.value = viewModel?.slotSuffix
viewModel?.showSlotIntentDialog?.value = false
}) {
Text("Active Slot")
}
}
)
}
LaunchedEffect(slotSuffix) {
val uri = viewModel!!.pendingFlashUri
if (uri != null && slotSuffix != null) {
if (viewModel?.isAb == true && slotSuffix == "_b")
{
viewModel?.slotB?.flashActionType = "flashAk3"
viewModel?.slotB?.flashActionURI = uri
viewModel?.slotB?.showConfirmDialog()
}
else
{
viewModel?.slotA?.flashActionType = "flashAk3"
viewModel?.slotA?.flashActionURI = uri
viewModel?.slotA?.showConfirmDialog()
}
navController.navigate("slot${slotSuffix}")
navController.navigate("slot${slotSuffix}/flash") {
popUpTo("slot${slotSuffix}")
}
viewModel!!.pendingFlashUri = null
viewModel!!.slotSuffixForFlash.value = null
}
}
}
}
}
public override fun onResume() {
super.onResume()
if (this::mainListener.isInitialized) {
if (!isAwaitingResult) {
mainListener.resume()
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt
================================================
package com.github.capntrips.kernelflasher
internal class MainListener(private val callback: () -> Unit) {
fun resume() {
callback.invoke()
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt
================================================
package com.github.capntrips.kernelflasher.common
import android.content.Context
import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex
import com.github.capntrips.kernelflasher.common.types.partitions.FstabEntry
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.serialization.json.Json
import java.io.File
import java.security.DigestOutputStream
import java.security.MessageDigest
object PartitionUtil {
val PartitionNames = listOf(
"boot",
"dtbo",
"init_boot",
"recovery",
"system_dlkm",
"vbmeta",
"vendor_boot",
"vendor_dlkm",
"vendor_kernel_boot"
)
val AvailablePartitions = mutableListOf<String>()
private var fileSystemManager: FileSystemManager? = null
private var bootParent: File? = null
fun init(context: Context, fileSystemManager: FileSystemManager) {
this.fileSystemManager = fileSystemManager
val fstabEntry = findPartitionFstabEntry(context, "boot")
if (fstabEntry != null) {
bootParent = File(fstabEntry.blkDevice).parentFile
}
val activeSlotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0]
for (partitionName in PartitionNames) {
val blockDevice = findPartitionBlockDevice(context, partitionName, activeSlotSuffix)
if (blockDevice != null && blockDevice.exists()) {
AvailablePartitions.add(partitionName)
}
}
}
private fun findPartitionFstabEntry(context: Context, partitionName: String): FstabEntry? {
val httools = File(context.filesDir, "httools_static")
val result = Shell.cmd("$httools dump $partitionName").exec().out
if (result.isNotEmpty() && result[0].trim().startsWith("{")) {
return Json.decodeFromString<FstabEntry>(result[0])
}
return null
}
fun isPartitionLogical(context: Context, partitionName: String): Boolean {
return findPartitionFstabEntry(context, partitionName)?.fsMgrFlags?.logical == true
}
fun findPartitionBlockDevice(context: Context, partitionName: String, slotSuffix: String): ExtendedFile? {
var blockDevice: ExtendedFile? = null
val fstabEntry = findPartitionFstabEntry(context, partitionName)
if (fstabEntry != null) {
if (fstabEntry.fsMgrFlags?.logical == true) {
if (fstabEntry.logicalPartitionName == "$partitionName$slotSuffix") {
blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice)
}
} else {
blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice)
if (blockDevice.name != "$partitionName$slotSuffix") {
blockDevice = fileSystemManager!!.getFile(blockDevice.parentFile, "$partitionName$slotSuffix")
}
}
}
if (blockDevice == null || !blockDevice.exists()) {
val siblingDevice = if (bootParent != null) fileSystemManager!!.getFile(bootParent!!, "$partitionName$slotSuffix") else null
val physicalDevice = fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix")
val logicalDevice = fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix")
if (siblingDevice?.exists() == true) {
blockDevice = physicalDevice
} else if (physicalDevice.exists()) {
blockDevice = physicalDevice
} else if (logicalDevice.exists()) {
blockDevice = logicalDevice
}
}
return blockDevice
}
@Suppress("unused")
fun partitionAvb(context: Context, partitionName: String): String {
val httools = File(context.filesDir, "httools_static")
val result = Shell.cmd("$httools avb $partitionName").exec().out
return if (result.isNotEmpty()) result[0] else ""
}
fun flashBlockDevice(image: ExtendedFile, blockDevice: ExtendedFile, hashAlgorithm: String): String {
val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt()
val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt()
if (partitionSize < imageSize) {
throw Error("Partition ${blockDevice.name} is smaller than image")
}
if (partitionSize > imageSize) {
Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice && sync").exec()
}
val messageDigest = MessageDigest.getInstance(hashAlgorithm)
image.newInputStream().use { inputStream ->
blockDevice.newOutputStream().use { outputStream ->
DigestOutputStream(outputStream, messageDigest).use { digestOutputStream ->
inputStream.copyTo(digestOutputStream)
}
}
}
return messageDigest.digest().toHex()
}
@Suppress("SameParameterValue")
fun flashLogicalPartition(context: Context, image: ExtendedFile, blockDevice: ExtendedFile, partitionName: String, slotSuffix: String, hashAlgorithm: String, addMessage: (message: String) -> Unit): String {
val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt()
val lptools = File(context.filesDir, "lptools_static")
Shell.cmd("$lptools remove ${partitionName}_kf").exec()
if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) {
if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) {
if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) {
val temporaryBlockDevice = fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf")
val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm)
if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix").exec().isSuccess) {
return hash
} else {
throw Error("Replacing $partitionName$slotSuffix failed")
}
} else {
throw Error("Remapping ${partitionName}_kf failed")
}
} else {
throw Error("Unmapping ${partitionName}_kf failed")
}
} else {
addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...")
val httools = File(context.filesDir, "httools_static")
if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) {
val verityBlockDevice = blockDevice.parentFile!!.getChildFile("${partitionName}-verity")
if (verityBlockDevice.exists()) {
if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) {
throw Error("Unmapping ${partitionName}-verity failed")
}
}
if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) {
if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)").exec().isSuccess) {
if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) {
val hash = flashBlockDevice(image, blockDevice, hashAlgorithm)
if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) {
return hash
} else {
throw Error("Mounting $partitionName failed")
}
} else {
throw Error("Remapping $partitionName$slotSuffix failed")
}
} else {
throw Error("Resizing $partitionName$slotSuffix failed")
}
} else {
throw Error("Unmapping $partitionName$slotSuffix failed")
}
} else {
throw Error("Unmounting $partitionName failed")
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt
================================================
package com.github.capntrips.kernelflasher.common.extensions
import kotlin.ByteArray
object ByteArray {
fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) }
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt
================================================
package com.github.capntrips.kernelflasher.common.extensions
import com.topjohnwu.superuser.nio.ExtendedFile
import java.io.InputStream
import java.io.InputStreamReader
import java.io.OutputStream
import java.nio.charset.Charset
object ExtendedFile {
private fun ExtendedFile.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = inputStream().reader(charset)
private fun ExtendedFile.writeBytes(array: kotlin.ByteArray): Unit = outputStream().use { it.write(array) }
fun ExtendedFile.readText(charset: Charset = Charsets.UTF_8): String = reader(charset).use { it.readText() }
@Suppress("unused")
fun ExtendedFile.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset))
fun ExtendedFile.inputStream(): InputStream = newInputStream()
fun ExtendedFile.outputStream(): OutputStream = newOutputStream()
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt
================================================
package com.github.capntrips.kernelflasher.common.types.backups
import com.github.capntrips.kernelflasher.common.types.partitions.Partitions
import kotlinx.serialization.Serializable
@Serializable
data class Backup(
val name: String,
val type: String,
val kernelVersion: String,
val bootSha1: String? = null,
val filename: String? = null,
val hashes: Partitions? = null,
val hashAlgorithm: String? = null
)
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt
================================================
package com.github.capntrips.kernelflasher.common.types.partitions
import kotlinx.serialization.Serializable
@Serializable
data class FsMgrFlags(
val logical: Boolean = false
)
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt
================================================
package com.github.capntrips.kernelflasher.common.types.partitions
import kotlinx.serialization.Serializable
@Serializable
data class FstabEntry(
val blkDevice: String,
val mountPoint: String,
val fsType: String,
val logicalPartitionName: String? = null,
val avb: Boolean = false,
val vbmetaPartition: String? = null,
val avbKeys: String? = null,
val fsMgrFlags: FsMgrFlags? = null
)
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt
================================================
package com.github.capntrips.kernelflasher.common.types.partitions
import kotlinx.serialization.Serializable
@Serializable
data class Partitions(
val boot: String? = null,
val dtbo: String? = null,
@Suppress("PropertyName") val init_boot: String? = null,
val recovery: String? = null,
@Suppress("PropertyName") val system_dlkm: String? = null,
val vbmeta: String? = null,
@Suppress("PropertyName") val vendor_boot: String? = null,
@Suppress("PropertyName") val vendor_dlkm: String? = null,
@Suppress("PropertyName") val vendor_kernel_boot: String? = null
) {
companion object {
fun from(sparseMap: Map<String, String>) = object {
val map = sparseMap.withDefault { null }
val boot by map
val dtbo by map
val init_boot by map
val recovery by map
val system_dlkm by map
val vbmeta by map
val vendor_boot by map
val vendor_dlkm by map
val vendor_kernel_boot by map
val partitions = Partitions(boot, dtbo, init_boot, recovery, system_dlkm, vbmeta, vendor_boot, vendor_dlkm, vendor_kernel_boot)
}.partitions
}
operator fun get(partition: String): String? {
return when (partition) {
"boot" -> boot
"dtbo" -> dtbo
"init_boot" -> init_boot
"recovery" -> recovery
"system_dlkm" -> system_dlkm
"vbmeta" -> vbmeta
"vendor_boot" -> vendor_boot
"vendor_dlkm" -> vendor_dlkm
"vendor_kernel_boot" -> vendor_kernel_boot
else -> null
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt
================================================
package com.github.capntrips.kernelflasher.common.types.room
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.github.capntrips.kernelflasher.common.types.room.updates.Update
import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateDao
@Database(entities = [Update::class], version = 1)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun updateDao(): UpdateDao
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt
================================================
package com.github.capntrips.kernelflasher.common.types.room
import androidx.room.TypeConverter
import java.util.Date
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {
return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? {
return date?.time
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt
================================================
package com.github.capntrips.kernelflasher.common.types.room.updates
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonTransformingSerializer
import kotlinx.serialization.json.buildJsonObject
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object DateSerializer : KSerializer<Date> {
override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(formatter.format(value))
override fun deserialize(decoder: Decoder): Date = formatter.parse(decoder.decodeString())!!
}
object UpdateSerializer : JsonTransformingSerializer<Update>(Update.serializer()) {
override fun transformSerialize(element: JsonElement): JsonElement {
require(element is JsonObject)
return buildJsonObject {
put("kernel", buildJsonObject {
put("name", element["kernelName"]!!)
put("version", element["kernelVersion"]!!)
put("link", element["kernelLink"]!!)
put("changelog_url", element["kernelChangelogUrl"]!!)
put("date", element["kernelDate"]!!)
put("sha1", element["kernelSha1"]!!)
})
if (element["supportLink"] != null) {
put("support", buildJsonObject {
put("link", element["supportLink"]!!)
})
}
}
}
override fun transformDeserialize(element: JsonElement): JsonElement {
require(element is JsonObject)
val kernel = element["kernel"]
val support = element["support"]
require(kernel is JsonObject)
require(support is JsonObject?)
return buildJsonObject {
put("kernelName", kernel["name"]!!)
put("kernelVersion", kernel["version"]!!)
put("kernelLink", kernel["link"]!!)
put("kernelChangelogUrl", kernel["changelog_url"]!!)
put("kernelDate", kernel["date"]!!)
put("kernelSha1", kernel["sha1"]!!)
if (support != null && support["link"] != null) {
put("supportLink", support["link"]!!)
}
}
}
}
@Entity
@Serializable
data class Update(
@PrimaryKey
@Transient
val id: Int? = null,
@ColumnInfo(name = "update_uri")
@Transient
var updateUri: String? = null,
@ColumnInfo(name = "kernel_name")
var kernelName: String,
@ColumnInfo(name = "kernel_version")
var kernelVersion: String,
@ColumnInfo(name = "kernel_link")
var kernelLink: String,
@ColumnInfo(name = "kernel_changelog_url")
var kernelChangelogUrl: String,
@ColumnInfo(name = "kernel_date")
@Serializable(DateSerializer::class)
var kernelDate: Date,
@ColumnInfo(name = "kernel_sha1")
var kernelSha1: String,
@ColumnInfo(name = "support_link")
var supportLink: String?,
@ColumnInfo(name = "last_updated")
@Transient
var lastUpdated: Date? = null,
)
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt
================================================
package com.github.capntrips.kernelflasher.common.types.room.updates
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
@Dao
interface UpdateDao {
@Query("""SELECT * FROM "update"""")
fun getAll(): List<Update>
@Query("""SELECT * FROM "update" WHERE id IN (:id)""")
fun load(id: Int): Update
@Insert
fun insert(update: Update): Long
@androidx.room.Update
fun update(update: Update)
@Delete
fun delete(update: Update)
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
// TODO: Remove when card is supported in material3: https://m3.material.io/components/cards/implementation/android
@Composable
fun Card(
shape: Shape = RoundedCornerShape(4.dp),
backgroundColor: Color = MaterialTheme.colorScheme.surface,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
border: BorderStroke? = null,
tonalElevation: Dp = 2.dp,
shadowElevation: Dp = 1.dp,
content: @Composable ColumnScope.() -> Unit
) {
Surface(
shape = shape,
color = backgroundColor,
contentColor = contentColor,
tonalElevation = tonalElevation,
shadowElevation = shadowElevation,
border = border
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(18.dp, (13.788).dp, 18.dp, 18.dp),
content = content
)
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun DataCard(
title: String,
button: @Composable (() -> Unit)? = null,
content: @Composable (ColumnScope.() -> Unit)? = null
) {
Card {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(0.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(0.dp, 9.dp, 8.dp, 9.dp).weight(1.0f),
text = title,
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.titleLarge
)
if (button != null) {
button()
}
}
if (content != null) {
Spacer(Modifier.height(10.dp))
content()
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
@Composable
fun DataRow(
label: String,
value: String,
labelColor: Color = Color.Unspecified,
labelStyle: TextStyle = MaterialTheme.typography.labelMedium,
valueColor: Color = Color.Unspecified,
valueStyle: TextStyle = MaterialTheme.typography.titleSmall,
mutableMaxWidth: MutableState<Int>? = null,
clickable: Boolean = false,
) {
Row {
val modifier = if (mutableMaxWidth != null) {
var maxWidth by mutableMaxWidth
Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
maxWidth = maxOf(maxWidth, placeable.width)
layout(width = maxWidth, height = placeable.height) {
placeable.placeRelative(0, 0)
}
}
.alignByBaseline()
} else {
Modifier
.alignByBaseline()
}
Text(
modifier = modifier,
text = label,
color = labelColor,
style = labelStyle
)
Spacer(Modifier.width(8.dp))
DataValue(value, valueColor, valueStyle, clickable)
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
@Composable
fun DataSet(
label: String,
labelColor: Color = Color.Unspecified,
labelStyle: TextStyle = MaterialTheme.typography.labelMedium,
content: @Composable (ColumnScope.() -> Unit)
) {
Text(
text = label,
color = labelColor,
style = labelStyle
)
Column(Modifier.padding(start = 16.dp)) {
content()
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataValue.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
@Composable
fun RowScope.DataValue(
value: String,
color: Color = Color.Unspecified,
style: TextStyle = MaterialTheme.typography.titleSmall,
clickable: Boolean = false,
) {
SelectionContainer(Modifier.alignByBaseline()) {
var clicked by remember { mutableStateOf(false) }
val modifier = if (clickable) {
Modifier
.clickable { clicked = !clicked }
.alignByBaseline()
} else {
Modifier
.alignByBaseline()
}
Text(
modifier = modifier,
text = value,
color = color,
style = style,
maxLines = if (clicked) Int.MAX_VALUE else 1,
overflow = if (clicked) TextOverflow.Visible else TextOverflow.Ellipsis
)
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
@Composable
fun DialogButton(
buttonText: String,
onClick: () -> Unit
) {
TextButton(
modifier = Modifier.padding(0.dp),
shape = RoundedCornerShape(4.0.dp),
contentPadding = PaddingValues(
horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp,
vertical = ButtonDefaults.ContentPadding.calculateTopPadding()
),
onClick = onClick
) {
Text(buttonText,
maxLines = 1,
color = MaterialTheme.colorScheme.primary
)
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import android.net.Uri
import android.provider.OpenableColumns
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import com.github.capntrips.kernelflasher.MainActivity
import kotlinx.serialization.ExperimentalSerializationApi
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@ExperimentalMaterial3Api
@ExperimentalSerializationApi
@ExperimentalUnitApi
@Composable
fun FlashButton(
buttonText: String,
validExtension: String,
callback: (uri: Uri) -> Unit
) {
val mainActivity = LocalContext.current as MainActivity
val result = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
result.value = it
if (it == null) {
mainActivity.isAwaitingResult = false
}
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
mainActivity.isAwaitingResult = true
launcher.launch("*/*")
}
) {
Text(buttonText)
}
result.value?.let {uri ->
if (mainActivity.isAwaitingResult) {
val contentResolver = mainActivity.contentResolver
val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (nameIndex != -1 && cursor.moveToFirst()) {
cursor.getString(nameIndex)
} else {
null
}
}
if (fileName != null && fileName.endsWith(validExtension, ignoreCase = true)) {
callback.invoke(uri)
}
else {
// Invalid file extension, show an error message or handle it
Toast.makeText(mainActivity.applicationContext, "Invalid file selected!", Toast.LENGTH_LONG).show()
}
}
mainActivity.isAwaitingResult = false
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.interaction.collectIsDraggedAsState
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
@ExperimentalUnitApi
@Composable
fun ColumnScope.FlashList(
cardTitle: String,
output: List<String>,
content: @Composable ColumnScope.() -> Unit
) {
val listState = rememberLazyListState()
var hasDragged by remember { mutableStateOf(false) }
val isDragged by listState.interactionSource.collectIsDraggedAsState()
if (isDragged) {
hasDragged = true
}
var shouldScroll = false
if (!hasDragged) {
if (listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != null) {
if (listState.layoutInfo.totalItemsCount - listState.layoutInfo.visibleItemsInfo.size > listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index!!) {
shouldScroll = true
}
}
}
LaunchedEffect(shouldScroll) {
listState.animateScrollToItem(output.size)
}
DataCard (cardTitle)
Spacer(Modifier.height(4.dp))
LazyColumn(
Modifier
.weight(1.0f)
.fillMaxSize()
.scrollbar(listState),
listState
) {
items(output) { message ->
Text(message,
style = LocalTextStyle.current.copy(
fontFamily = FontFamily.Monospace,
fontSize = TextUnit(12.0f, TextUnitType.Sp),
lineHeight = TextUnit(18.0f, TextUnitType.Sp)
)
)
}
}
content()
}
// https://stackoverflow.com/a/68056586/434343
fun Modifier.scrollbar(
state: LazyListState,
width: Dp = 6.dp
): Modifier = composed {
var visibleItemsCountChanged = false
var visibleItemsCount by remember { mutableIntStateOf(state.layoutInfo.visibleItemsInfo.size) }
if (visibleItemsCount != state.layoutInfo.visibleItemsInfo.size) {
visibleItemsCountChanged = true
visibleItemsCount = state.layoutInfo.visibleItemsInfo.size
}
val hidden = state.layoutInfo.visibleItemsInfo.size == state.layoutInfo.totalItemsCount
val targetAlpha = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0.5f else 0f
val delay = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0 else 250
val duration = if (hidden || visibleItemsCountChanged) 0 else if (state.isScrollInProgress) 150 else 500
val alpha by animateFloatAsState(
targetValue = targetAlpha,
animationSpec = tween(delayMillis = delay, durationMillis = duration)
)
drawWithContent {
drawContent()
val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index
val needDrawScrollbar = state.isScrollInProgress || visibleItemsCountChanged || alpha > 0.0f
if (needDrawScrollbar && firstVisibleElementIndex != null) {
val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
val scrollbarOffsetY = firstVisibleElementIndex * elementHeight
val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
drawRoundRect(
color = Color.Gray,
topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY),
size = Size(width.toPx(), scrollbarHeight),
cornerRadius = CornerRadius(width.toPx(), width.toPx()),
alpha = alpha
)
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel
@ExperimentalMaterial3Api
@Composable
fun SlotCard(
title: String,
viewModel: SlotViewModel,
navController: NavController,
isSlotScreen: Boolean = false,
showDlkm: Boolean = true,
) {
DataCard (
title = "$title ${if(viewModel.isActive && viewModel.slotSuffix !="") "[${stringResource(R.string.active)}]" else ""}",
button = {
if (!isSlotScreen) {
AnimatedVisibility(!viewModel.isRefreshing.value) {
ViewButton {
navController.navigate("slot${viewModel.slotSuffix}")
}
}
}
}
) {
val cardWidth = remember { mutableIntStateOf(0) }
if (!viewModel.sha1.isNullOrEmpty()) {
DataRow(
label = stringResource(R.string.boot_sha1),
value = viewModel.sha1!!.substring(0, 8),
valueStyle = MaterialTheme.typography.titleSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium
),
mutableMaxWidth = cardWidth
)
}
AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.slotInfo.bootImgInfo.kernelVersion != null) {
DataRow(
label = stringResource(R.string.kernel_version),
value = viewModel.slotInfo.bootImgInfo.kernelVersion ?: "",
mutableMaxWidth = cardWidth,
clickable = true
)
}
if (showDlkm && viewModel.hasVendorDlkm) {
var vendorDlkmValue = stringResource(R.string.not_found)
if (viewModel.isVendorDlkmMapped) {
vendorDlkmValue = if (viewModel.isVendorDlkmMounted) {
String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.mounted))
} else {
String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.unmounted))
}
}
DataRow(stringResource(R.string.vendor_dlkm), vendorDlkmValue, mutableMaxWidth = cardWidth)
}
DataRow(
label = stringResource(R.string.boot_fmt),
value = viewModel.slotInfo.bootImgInfo.bootFmt ?: stringResource(R.string.not_found),
mutableMaxWidth = cardWidth
)
DataRow(
label = if (viewModel.slotInfo.ramdiskInfo.ramdiskLocation == "init_boot.img") stringResource(R.string.init_boot_fmt)
else if (viewModel.slotInfo.ramdiskInfo.ramdiskLocation == "vendor_boot.img") stringResource(R.string.vendor_boot_fmt)
else stringResource(R.string.ramdisk_fmt),
value = viewModel.slotInfo.ramdiskInfo.ramdiskFmt ?: stringResource(R.string.not_found),
mutableMaxWidth = cardWidth
)
if(isSlotScreen && viewModel.slotSuffix != "")
{
DataRow(
label = stringResource(R.string.unbootable),
value = viewModel.slotInfo.bootSlotInfo.unbootable ?: stringResource(R.string.not_found),
mutableMaxWidth = cardWidth,
valueColor = if (viewModel.slotInfo.bootSlotInfo.unbootable == "Yes") Color.Red else Color.Unspecified
)
DataRow(
label = stringResource(R.string.successful),
value = viewModel.slotInfo.bootSlotInfo.successful ?: stringResource(R.string.not_found),
mutableMaxWidth = cardWidth,
valueColor = if (viewModel.slotInfo.bootSlotInfo.successful == "No") Color.Red else Color.Unspecified
)
}
if (!viewModel.isRefreshing.value && viewModel.hasError) {
Row {
DataValue(
value = viewModel.error ?: "",
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.titleSmall,
clickable = true
)
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt
================================================
package com.github.capntrips.kernelflasher.ui.components
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.github.capntrips.kernelflasher.R
@Composable
fun ViewButton(
onClick: () -> Unit
) {
TextButton(
modifier = Modifier.padding(0.dp),
shape = RoundedCornerShape(4.0.dp),
contentPadding = PaddingValues(
horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp,
vertical = ButtonDefaults.ContentPadding.calculateTopPadding()
),
onClick = onClick
) {
Text(stringResource(R.string.view), maxLines = 1)
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel
import kotlinx.serialization.ExperimentalSerializationApi
@ExperimentalMaterialApi
@ExperimentalMaterial3Api
@ExperimentalSerializationApi
@Composable
fun RefreshableScreen(
viewModel: MainViewModel,
navController: NavController,
swipeEnabled: Boolean = false,
content: @Composable ColumnScope.() -> Unit
) {
val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues()
val navigationBars = WindowInsets.navigationBars.asPaddingValues()
val context = LocalContext.current
val state = rememberPullRefreshState(viewModel.isRefreshing, onRefresh = {
viewModel.refresh(context)
})
Scaffold(
topBar = {
Box(
Modifier
.fillMaxWidth()
.padding(statusBar)) {
if (navController.previousBackStackEntry != null) {
AnimatedVisibility(
!viewModel.isRefreshing,
enter = fadeIn(),
exit = fadeOut()
) {
IconButton(
onClick = { navController.popBackStack() },
modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp)
) {
Icon(
Icons.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
tint = MaterialTheme.colorScheme.onSurface
)
}
}
}
Box(
Modifier
.fillMaxWidth()
.padding(16.dp)) {
Text(
modifier = Modifier.align(Alignment.Center),
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineSmall
)
}
}
}
) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.pullRefresh(state, swipeEnabled)
.fillMaxSize(),
) {
Column(
modifier = Modifier
.padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding())
.fillMaxSize()
.verticalScroll(rememberScrollState()),
content = content
)
PullRefreshIndicator(
viewModel.isRefreshing,
state = state,
modifier = Modifier.align(Alignment.TopCenter),
backgroundColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.primaryContainer,
scale = true
)
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.backups
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.common.PartitionUtil
import com.github.capntrips.kernelflasher.ui.components.DataCard
import com.github.capntrips.kernelflasher.ui.components.DataRow
import com.github.capntrips.kernelflasher.ui.components.DataSet
import com.github.capntrips.kernelflasher.ui.components.ViewButton
@ExperimentalMaterial3Api
@Composable
fun ColumnScope.BackupsContent(
viewModel: BackupsViewModel,
navController: NavController
) {
val context = LocalContext.current
val monoStyle = MaterialTheme.typography.titleSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium
)
if (viewModel.currentBackup != null && viewModel.backups.containsKey(viewModel.currentBackup)) {
DataCard (viewModel.currentBackup!!) {
val cardWidth = remember { mutableIntStateOf(0) }
val backupId = viewModel.currentBackup!!
val currentBackup = viewModel.backups[backupId]
if(currentBackup == null) return@DataCard
DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth)
DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true)
if (currentBackup.type == "raw") {
currentBackup.bootSha1?.takeIf { it.length >= 8 }?.let { sha1 ->
DataRow(
label = stringResource(R.string.boot_sha1),
value = sha1.substring(0, 8),
valueStyle = monoStyle,
mutableMaxWidth = cardWidth
)
}
if (currentBackup.hashes != null) {
val hashWidth = remember { mutableIntStateOf(0) }
DataSet(stringResource(R.string.hashes)) {
for (partitionName in PartitionUtil.PartitionNames) {
val hash = currentBackup.hashes[partitionName]
if (hash != null) {
DataRow(
label = partitionName,
value = hash.takeIf { it.isNotEmpty() }?.substring(0, 8) ?: "Hash not found!",
valueStyle = monoStyle,
mutableMaxWidth = hashWidth
)
}
}
}
}
}
}
AnimatedVisibility(!viewModel.isRefreshing) {
Column {
Spacer(Modifier.height(5.dp))
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.delete(context) { navController.popBackStack() } }
) {
Text(stringResource(R.string.delete))
}
}
}
} else {
DataCard(stringResource(R.string.backups))
AnimatedVisibility(viewModel.needsMigration) {
Column {
Spacer(Modifier.height(5.dp))
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.migrate(context) }
) {
Text(stringResource(R.string.migrate))
}
}
}
if (viewModel.backups.isNotEmpty()) {
for (id in viewModel.backups.keys.sortedByDescending { it }) {
val currentBackup = viewModel.backups[id]!!
Spacer(Modifier.height(16.dp))
DataCard(
title = id,
button = {
AnimatedVisibility(!viewModel.isRefreshing) {
Column {
ViewButton(onClick = {
navController.navigate("backups/$id")
})
}
}
}
) {
val cardWidth = remember { mutableIntStateOf(0) }
if (currentBackup.type == "raw") {
currentBackup.bootSha1?.takeIf { it.length >= 8 }?.let { sha1 ->
DataRow(
label = stringResource(R.string.boot_sha1),
value = sha1.substring(0, 8),
valueStyle = monoStyle,
mutableMaxWidth = cardWidth
)
}
}
DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true)
}
}
} else {
Spacer(Modifier.height(32.dp))
Text(
stringResource(R.string.no_backups_found),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic
)
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.backups
import android.annotation.SuppressLint
import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.SharedViewModels
import com.github.capntrips.kernelflasher.common.PartitionUtil
import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream
import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.readText
import com.github.capntrips.kernelflasher.common.types.backups.Backup
import com.github.capntrips.kernelflasher.common.types.partitions.Partitions
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlin.DeprecationLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import java.io.File
import java.io.FileInputStream
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.Properties
class BackupsViewModel(
context: Context,
private val fileSystemManager: FileSystemManager,
private val navController: NavController,
private val _isRefreshing: MutableState<Boolean>,
private val _backups: MutableMap<String, Backup>
) : ViewModel() {
companion object {
const val TAG: String = "KernelFlasher/BackupsState"
}
private val _restoreOutput: SnapshotStateList<String> = mutableStateListOf()
var currentBackup: String? = null
set(value) {
if (value != field) {
if (_backups[value]?.hashes != null) {
PartitionUtil.AvailablePartitions.forEach { partitionName ->
if (_backups[value]!!.hashes!![partitionName] != null) {
_backupPartitions[partitionName] = true
}
}
}
field = value
}
}
var wasRestored: Boolean? = null
private val _backupPartitions: SnapshotStateMap<String, Boolean> = mutableStateMapOf()
private val hashAlgorithm: String = "SHA-256"
@Deprecated("Backup migration will be removed in the first stable release", level = DeprecationLevel.WARNING)
private var _needsMigration: MutableState<Boolean> = mutableStateOf(false)
val restoreOutput: List<String>
get() = _restoreOutput
val backupPartitions: MutableMap<String, Boolean>
get() = _backupPartitions
val isRefreshing: Boolean
get() = _isRefreshing.value
val backups: Map<String, Backup>
get() = _backups
@Deprecated("Backup migration will be removed in the first stable release")
val needsMigration: Boolean
get() = _needsMigration.value
init {
refresh(context)
}
fun refresh(context: Context) {
val oldDir = context.getExternalFilesDir(null)
val oldBackupsDir = File(oldDir, "backups")
// Deprecated: Backup migration will be removed in the first stable release
_needsMigration.value = oldBackupsDir.exists() && oldBackupsDir.listFiles()?.size!! > 0
@SuppressLint("SdCardPath")
val externalDir = File("/sdcard/KernelFlasher")
val backupsDir = fileSystemManager.getFile("$externalDir/backups")
if (backupsDir.exists()) {
val children = backupsDir.listFiles()
if (children != null) {
for (child in children.sortedByDescending{it.name}) {
if (!child.isDirectory) {
continue
}
val jsonFile = child.getChildFile("backup.json")
if (jsonFile.exists()) {
_backups[child.name] = Json.decodeFromString(jsonFile.readText())
}
}
}
}
}
private fun launch(block: suspend () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
_isRefreshing.value = true
try {
block()
} catch (e: Exception) {
withContext (Dispatchers.Main) {
Log.e(TAG, e.message, e)
navController.navigate("error/${e.message}") {
popUpTo("main")
}
}
}
_isRefreshing.value = false
}
}
@Suppress("SameParameterValue")
private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
Log.d(TAG, message)
if (!shouldThrow) {
viewModelScope.launch(Dispatchers.Main) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
} else {
throw Exception(message)
}
}
fun clearCurrent() {
currentBackup = null
clearRestore()
}
private fun addMessage(message: String) {
viewModelScope.launch(Dispatchers.Main) {
_restoreOutput.add(message)
}
}
@Suppress("FunctionName")
private fun _clearRestore() {
_restoreOutput.clear()
wasRestored = null
}
private fun clearRestore() {
_clearRestore()
_backupPartitions.clear()
}
@Suppress("unused")
@SuppressLint("SdCardPath")
fun saveLog(context: Context) {
launch {
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
val log = File("/sdcard/Download/restore-log--$now.log")
log.writeText(restoreOutput.joinToString("\n"))
if (log.exists()) {
log(context, "Saved restore log to $log")
} else {
log(context, "Failed to save $log", shouldThrow = true)
}
}
}
private fun restorePartitions(context: Context, source: ExtendedFile, slotSuffix: String): Partitions? {
val partitions = HashMap<String, String>()
for (partitionName in PartitionUtil.PartitionNames) {
if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) {
val image = source.getChildFile("$partitionName.img")
if (image.exists()) {
val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix)
if (blockDevice != null && blockDevice.exists()) {
addMessage("Restoring $partitionName")
partitions[partitionName] = if (PartitionUtil.isPartitionLogical(context, partitionName)) {
PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message ->
addMessage(message)
}
} else {
PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm)
}
} else {
log(context, "Partition $partitionName was not found", shouldThrow = true)
}
}
}
}
if (partitions.isNotEmpty()) {
return Partitions.from(partitions)
}
return null
}
fun restore(context: Context, slotSuffix: String) {
launch {
_clearRestore()
@SuppressLint("SdCardPath")
val externalDir = File("/sdcard/KernelFlasher")
val backupsDir = fileSystemManager.getFile("$externalDir/backups")
val backupDir = backupsDir.getChildFile(currentBackup!!)
if (!backupDir.exists()) {
log(context, "Backup $currentBackup does not exists", shouldThrow = true)
return@launch
}
addMessage("Restoring backup $currentBackup")
val hashes = restorePartitions(context, backupDir, slotSuffix)
if (hashes == null) {
log(context, "No partitions restored", shouldThrow = true)
}
addMessage("Backup $currentBackup restored")
wasRestored = true
}
}
fun delete(context: Context, callback: () -> Unit) {
launch {
@SuppressLint("SdCardPath")
val externalDir = File("/sdcard/KernelFlasher")
val backupsDir = fileSystemManager.getFile("$externalDir/backups")
val backupDir = backupsDir.getChildFile(currentBackup!!)
if (!backupDir.exists()) {
log(context, "Backup $currentBackup does not exists", shouldThrow = true)
return@launch
}
backupDir.deleteRecursively()
_backups.remove(currentBackup!!)
withContext(Dispatchers.Main) {
callback.invoke()
}
}
}
@OptIn(ExperimentalSerializationApi::class)
@SuppressLint("SdCardPath")
@Deprecated("Backup migration will be removed in the first stable release")
fun migrate(context: Context) {
launch {
val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher")
if (!externalDir.exists()) {
if (!externalDir.mkdir()) {
log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true)
}
}
val backupsDir = externalDir.getChildFile("backups")
if (!backupsDir.exists()) {
if (!backupsDir.mkdir()) {
log(context, "Failed to create backups dir", shouldThrow = true)
}
}
val oldDir = context.getExternalFilesDir(null)
val oldBackupsDir = File(oldDir, "backups")
if (oldBackupsDir.exists()) {
val indentedJson = Json { prettyPrint = true }
val children = oldBackupsDir.listFiles()
if (children != null) {
for (child in children.sortedByDescending{it.name}) {
if (!child.isDirectory) {
child.delete()
continue
}
val propFile = File(child, "backup.prop")
@Suppress("BlockingMethodInNonBlockingContext")
val inputStream = FileInputStream(propFile)
val props = Properties()
@Suppress("BlockingMethodInNonBlockingContext")
props.load(inputStream)
val name = child.name
val type = props.getProperty("type", "raw")
val kernelVersion = props.getProperty("kernel")
val bootSha1 = if (type == "raw") props.getProperty("sha1") else null
val filename = if (type == "ak3") "ak3.zip" else null
propFile.delete()
val dest = backupsDir.getChildFile(child.name)
Shell.cmd("mv $child $dest").exec()
if (!dest.exists()) {
throw Error("Too slow")
}
val jsonFile = dest.getChildFile("backup.json")
val backup = Backup(name, type, kernelVersion, bootSha1, filename)
jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) }
_backups[name] = backup
}
}
oldBackupsDir.delete()
}
SharedViewModels.mainViewModel.markRefreshNeeded()
refresh(context)
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.backups
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.common.PartitionUtil
import com.github.capntrips.kernelflasher.ui.components.DataCard
import com.github.capntrips.kernelflasher.ui.components.DataRow
import com.github.capntrips.kernelflasher.ui.components.DataSet
import com.github.capntrips.kernelflasher.ui.components.FlashList
import com.github.capntrips.kernelflasher.ui.components.SlotCard
import com.github.capntrips.kernelflasher.ui.components.ViewButton
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel
@ExperimentalMaterial3Api
@ExperimentalUnitApi
@Composable
fun ColumnScope.SlotBackupsContent(
slotViewModel: SlotViewModel,
backupsViewModel: BackupsViewModel,
slotSuffix: String,
navController: NavController
) {
val context = LocalContext.current
val monoStyle = MaterialTheme.typography.titleSmall.copy(
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.Medium
)
val currentRoute = navController.currentDestination?.route.orEmpty()
if (!currentRoute.contains("/backups/{backupId}/restore")) {
SlotCard(
title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot),
viewModel = slotViewModel,
navController = navController,
isSlotScreen = true,
showDlkm = false,
)
Spacer(Modifier.height(16.dp))
if (backupsViewModel.currentBackup != null && backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!)
DataCard(backupsViewModel.currentBackup!!) {
val cardWidth = remember { mutableIntStateOf(0) }
DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth)
DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true)
if (currentBackup.type == "raw") {
currentBackup.bootSha1?.takeIf { it.length >= 8 }?.let { sha1 ->
DataRow(
label = stringResource(R.string.boot_sha1),
value = sha1.substring(0, 8),
valueStyle = monoStyle,
mutableMaxWidth = cardWidth
)
}
if (currentBackup.hashes != null) {
val hashWidth = remember { mutableIntStateOf(0) }
DataSet(stringResource(R.string.hashes)) {
for (partitionName in PartitionUtil.PartitionNames) {
val hash = currentBackup.hashes[partitionName]
if (hash != null) {
DataRow(
label = partitionName,
value = hash.takeIf { it.isNotEmpty() && it.length >= 8 }?.substring(0, 8) ?: "Hash not found!",
valueStyle = monoStyle,
mutableMaxWidth = hashWidth
)
}
}
}
}
}
}
AnimatedVisibility(!slotViewModel.isRefreshing.value) {
Column {
Spacer(Modifier.height(5.dp))
if (slotViewModel.isActive) {
if (currentBackup.type == "raw") {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore")
}
) {
Text(stringResource(R.string.restore))
}
} else if (currentBackup.type == "ak3") {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
slotViewModel.flashAk3(context, backupsViewModel.currentBackup!!, currentBackup.filename!!)
navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") {
popUpTo("slot$slotSuffix")
}
}
) {
Text(stringResource(R.string.flash))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
slotViewModel.flashAk3_mkbootfs(context, backupsViewModel.currentBackup!!, currentBackup.filename!!)
navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") {
popUpTo("slot$slotSuffix")
}
}
) {
Text(stringResource(R.string.flash_ak3_zip_mkbootfs))
}
}
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { backupsViewModel.delete(context) { navController.popBackStack() } }
) {
Text(stringResource(R.string.delete))
}
}
}
} else {
DataCard(stringResource(R.string.backups))
val backups = backupsViewModel.backups.filter { it.value.bootSha1.isNullOrEmpty() || it.value.bootSha1.equals(slotViewModel.sha1) || it.value.type == "ak3" }
if (backups.isNotEmpty()) {
for (id in backups.keys.sortedByDescending { it }) {
Spacer(Modifier.height(16.dp))
DataCard(
title = id,
button = {
AnimatedVisibility(!slotViewModel.isRefreshing.value) {
ViewButton(onClick = {
navController.navigate("slot$slotSuffix/backups/$id")
})
}
}
) {
DataRow(stringResource(R.string.kernel_version), backups[id]!!.kernelVersion, clickable = true)
}
}
} else {
Spacer(Modifier.height(32.dp))
Text(
stringResource(R.string.no_backups_found),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic
)
}
}
} else if (navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/restore")) {
DataCard (stringResource(R.string.restore))
Spacer(Modifier.height(5.dp))
val disabledColor = ButtonDefaults.buttonColors(
Color.Transparent,
MaterialTheme.colorScheme.onSurface
)
val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!)
if (currentBackup.hashes != null) {
for (partitionName in PartitionUtil.PartitionNames) {
val hash = currentBackup.hashes[partitionName]
if (hash != null) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth()
.alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f),
shape = RoundedCornerShape(4.dp),
colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor,
enabled = backupsViewModel.backupPartitions[partitionName] != null,
onClick = {
backupsViewModel.backupPartitions[partitionName] = !backupsViewModel.backupPartitions[partitionName]!!
},
) {
Box(Modifier.fillMaxWidth()) {
Checkbox(backupsViewModel.backupPartitions[partitionName] == true, null,
Modifier
.align(Alignment.CenterStart)
.offset(x = -(16.dp)))
Text(partitionName, Modifier.align(Alignment.Center))
}
}
}
}
} else {
Text(
stringResource(R.string.partition_selection_unavailable),
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontStyle = FontStyle.Italic
)
Spacer(Modifier.height(5.dp))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
backupsViewModel.restore(context, slotSuffix)
navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") {
popUpTo("slot$slotSuffix")
}
},
enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none {
currentBackup.hashes[it] != null && backupsViewModel.backupPartitions[it] == null
} && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty())
) {
Text(stringResource(R.string.restore))
}
} else {
FlashList(
stringResource(R.string.restore),
backupsViewModel.restoreOutput
) {
AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) {
Column {
if (backupsViewModel.wasRestored != false) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { navController.navigate("reboot") }
) {
Text(stringResource(R.string.reboot))
}
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.error
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.github.capntrips.kernelflasher.ui.theme.Orange500
@ExperimentalMaterial3Api
@Composable
fun ErrorScreen(message: String) {
Scaffold { paddingValues ->
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
Icons.Filled.Warning,
modifier = Modifier
.width(48.dp)
.height(48.dp),
tint = Orange500,
contentDescription = message
)
Spacer(Modifier.height(8.dp))
Text(
message,
modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp),
style = MaterialTheme.typography.titleLarge,
)
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.main
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.ui.components.DataCard
import com.github.capntrips.kernelflasher.ui.components.DataRow
import com.github.capntrips.kernelflasher.ui.components.SlotCard
import kotlinx.serialization.ExperimentalSerializationApi
@ExperimentalMaterial3Api
@ExperimentalSerializationApi
@Composable
fun ColumnScope.MainContent(
viewModel: MainViewModel,
navController: NavController
) {
val context = LocalContext.current
DataCard (title = stringResource(R.string.device)) {
val cardWidth = remember { mutableIntStateOf(0) }
DataRow(stringResource(R.string.model), "${Build.MODEL} (${Build.DEVICE})", mutableMaxWidth = cardWidth)
DataRow(stringResource(R.string.build_number), Build.ID, mutableMaxWidth = cardWidth)
DataRow(stringResource(R.string.kernel_version), viewModel.kernelVersion, mutableMaxWidth = cardWidth, clickable = true)
if (viewModel.isAb)
DataRow(stringResource(R.string.slot_suffix), viewModel.slotSuffix, mutableMaxWidth = cardWidth)
if(viewModel.susfsVersion != "v0.0.0" && viewModel.susfsVersion != "Invalid")
DataRow(stringResource(R.string.susfs_version), viewModel.susfsVersion, mutableMaxWidth = cardWidth)
if(viewModel.halInfo != "")
DataRow("Boot HAL version", viewModel.halInfo, mutableMaxWidth = cardWidth)
}
Spacer(Modifier.height(16.dp))
SlotCard(
title = stringResource(if (viewModel.isAb) R.string.slot_a else R.string.slot),
viewModel = viewModel.slotA,
navController = navController
)
if (viewModel.isAb) {
Spacer(Modifier.height(16.dp))
SlotCard(
title = stringResource(R.string.slot_b),
viewModel = viewModel.slotB!!,
navController = navController
)
}
Spacer(Modifier.height(16.dp))
AnimatedVisibility(!viewModel.isRefreshing) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { navController.navigate("backups") }
) {
Text(stringResource(R.string.backups))
}
}
AnimatedVisibility(!viewModel.isRefreshing) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { navController.navigate("updates") }
) {
Text(stringResource(R.string.updates))
}
}
if (viewModel.hasRamoops) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.saveRamoops(context) }
) {
Text(stringResource(R.string.save_ramoops))
}
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.saveDmesg(context) }
) {
Text(stringResource(R.string.save_dmesg))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.saveLogcat(context) }
) {
Text(stringResource(R.string.save_logcat))
}
AnimatedVisibility(!viewModel.isRefreshing) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { navController.navigate("reboot") }
) {
Text(stringResource(R.string.reboot))
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.main
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.common.PartitionUtil
import com.github.capntrips.kernelflasher.common.types.backups.Backup
import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsViewModel
import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootViewModel
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewModel
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import java.io.File
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@ExperimentalSerializationApi
class MainViewModel(
context: Context,
fileSystemManager: FileSystemManager,
private val navController: NavController
) : ViewModel() {
companion object {
const val TAG: String = "KernelFlasher/MainViewModel"
}
val slotSuffix: String
val kernelVersion: String
val halInfo: String
val susfsVersion: String
val isAb: Boolean
val slotA: SlotViewModel
val slotB: SlotViewModel?
val backups: BackupsViewModel
val updates: UpdatesViewModel
val reboot: RebootViewModel
val hasRamoops: Boolean
private val _isRefreshing: MutableState<Boolean> = mutableStateOf(true)
private val _isRefreshRequired = mutableStateOf(true)
private var _error: String? = null
private var _backups: MutableMap<String, Backup> = mutableMapOf()
var showSlotIntentDialog: MutableState<Boolean> = mutableStateOf(false)
var pendingFlashUri: Uri? = null
var slotSuffixForFlash = mutableStateOf<String?>(null)
val isRefreshing: Boolean
get() = _isRefreshing.value
val isRefreshRequired: Boolean
get() = _isRefreshRequired.value
val hasError: Boolean
get() = _error != null
val error: String
get() = _error!!
fun markRefreshNeeded() {
_isRefreshRequired.value = true
}
data class UpdateDialogData(
val title: String,
val changelog: List<String>,
val onConfirm: () -> Unit
)
var updateDialogData by mutableStateOf<UpdateDialogData?>(null)
private set
fun showUpdateDialog(title: String, changelog: List<String>, onConfirm: () -> Unit) {
updateDialogData = UpdateDialogData(title, changelog, onConfirm)
}
fun hideUpdateDialog() {
updateDialogData = null
}
init {
PartitionUtil.init(context, fileSystemManager)
val bootctl = File(context.filesDir, "bootctl")
halInfo = runCatching { Shell.cmd("$bootctl hal-info").exec().out[0].substringAfter("HAL Version: ").trim() }
.recoverCatching { "" }
.getOrDefault("")
kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0]
susfsVersion = runCatching { Shell.cmd("susfsd version").exec().out[0] }
.recoverCatching { Shell.cmd("ksu_susfs show version").exec().out[0] }
.getOrDefault("v0.0.0")
slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0]
backups = BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups)
updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing)
reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing)
// https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:bootable/recovery/recovery.cpp;l=320
isAb = slotSuffix.isNotEmpty()
if (isAb) {
val bootA = PartitionUtil.findPartitionBlockDevice(context, "boot", "_a")!!
val bootB = PartitionUtil.findPartitionBlockDevice(context, "boot", "_b")!!
val initBootA = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_a")
val initBootB = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_b")
slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_a", "_a", bootA, initBootA, _backups)
slotB = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_b", "_b", bootB, initBootB, _backups)
} else {
val boot = PartitionUtil.findPartitionBlockDevice(context, "boot", "")!!
val initBoot = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "")
slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, true, "", boot, initBoot, _backups)
if (slotA.hasError) {
_error = slotA.error
}
slotB = null
}
hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists()
_isRefreshing.value = false
_isRefreshRequired.value = false
}
fun refresh(context: Context) {
if (!isRefreshRequired) return
launch {
slotA.refresh(context)
if (isAb) {
slotB!!.refresh(context)
}
backups.refresh(context)
_isRefreshRequired.value = false
}
}
private fun launch(block: suspend () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.Main) {
_isRefreshing.value = true
}
try {
block()
} catch (e: Exception) {
withContext (Dispatchers.Main) {
Log.e(TAG, e.message, e)
navController.navigate("error/${e.message}") {
popUpTo("main")
}
}
}
viewModelScope.launch(Dispatchers.Main) {
_isRefreshing.value = false
}
}
}
@Suppress("SameParameterValue")
private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
Log.d(TAG, message)
if (!shouldThrow) {
viewModelScope.launch(Dispatchers.Main) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
} else {
throw Exception(message)
}
}
fun saveRamoops(context: Context) {
launch {
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
@SuppressLint("SdCardPath")
val ramoops = File("/sdcard/Download/console-ramoops--$now.log")
Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec()
if (ramoops.exists()) {
log(context, "Saved ramoops to $ramoops")
} else {
log(context, "Failed to save $ramoops", shouldThrow = true)
}
}
}
fun saveDmesg(context: Context) {
launch {
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
@SuppressLint("SdCardPath")
val dmesg = File("/sdcard/Download/dmesg--$now.log")
Shell.cmd("dmesg > $dmesg").exec()
if (dmesg.exists()) {
log(context, "Saved dmesg to $dmesg")
} else {
log(context, "Failed to save $dmesg", shouldThrow = true)
}
}
}
fun saveLogcat(context: Context) {
launch {
val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
@SuppressLint("SdCardPath")
val logcat = File("/sdcard/Download/logcat--$now.log")
Shell.cmd("logcat -d > $logcat").exec()
if (logcat.exists()) {
log(context, "Saved logcat to $logcat")
} else {
log(context, "Failed to save $logcat", shouldThrow = true)
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.reboot
import android.os.Build
import android.os.PowerManager
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
@Suppress("UnusedReceiverParameter")
@Composable
fun ColumnScope.RebootContent(
viewModel: RebootViewModel,
@Suppress("UNUSED_PARAMETER") ignoredNavController: NavController
) {
val context = LocalContext.current
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.rebootSystem() }
) {
Text(stringResource(R.string.reboot))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.rebootRecovery() }
) {
Text(stringResource(R.string.reboot_recovery))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.rebootBootloader() }
) {
Text(stringResource(R.string.reboot_bootloader))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.rebootDownload() }
) {
Text(stringResource(R.string.reboot_download))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.rebootEdl() }
) {
Text(stringResource(R.string.reboot_edl))
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.reboot
import android.content.Context
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class RebootViewModel(
@Suppress("UNUSED_PARAMETER") ignoredContext: Context,
@Suppress("unused") private val fileSystemManager: FileSystemManager,
private val navController: NavController,
private val _isRefreshing: MutableState<Boolean>
) : ViewModel() {
companion object {
const val TAG: String = "KernelFlasher/RebootState"
}
val isRefreshing: Boolean
get() = _isRefreshing.value
private fun launch(block: suspend () -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
_isRefreshing.value = true
try {
block()
} catch (e: Exception) {
withContext (Dispatchers.Main) {
Log.e(TAG, e.message, e)
navController.navigate("error/${e.message}") {
popUpTo("main")
}
}
}
_isRefreshing.value = false
}
}
private fun reboot(destination: String = "") {
launch {
// https://github.com/topjohnwu/Magisk/blob/v25.2/app/src/main/java/com/topjohnwu/magisk/ktx/XSU.kt#L11-L15
if (destination == "recovery") {
// https://github.com/topjohnwu/Magisk/pull/5637
Shell.cmd("/system/bin/input keyevent 26").submit()
}
Shell.cmd("/system/bin/svc power reboot $destination || /system/bin/reboot $destination").submit()
}
}
fun rebootSystem() {
reboot()
}
fun rebootRecovery() {
reboot("recovery")
}
fun rebootBootloader() {
reboot("bootloader")
}
fun rebootDownload() {
reboot("download")
}
fun rebootEdl() {
reboot("edl")
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.slot
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.ui.components.SlotCard
@ExperimentalAnimationApi
@ExperimentalMaterial3Api
@ExperimentalUnitApi
@Composable
fun ColumnScope.SlotContent(
viewModel: SlotViewModel,
slotSuffix: String,
navController: NavController
) {
val context = LocalContext.current
SlotCard(
title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot),
viewModel = viewModel,
navController = navController,
isSlotScreen = true
)
AnimatedVisibility(!viewModel.isRefreshing.value) {
Column {
Spacer(Modifier.height(5.dp))
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
navController.navigate("slot$slotSuffix/flash")
}
) {
Text(stringResource(R.string.flash))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
viewModel.clearFlash(context)
navController.navigate("slot$slotSuffix/backup")
}
) {
Text(stringResource(R.string.backup))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
navController.navigate("slot$slotSuffix/backups")
}
) {
Text(stringResource(R.string.restore))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { if (!viewModel.isRefreshing.value) viewModel.getKernel(context) }
) {
Text(stringResource(R.string.check_kernel_version))
}
if (viewModel.hasVendorDlkm) {
AnimatedVisibility(!viewModel.isRefreshing.value) {
AnimatedVisibility(viewModel.isVendorDlkmMounted) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.unmountVendorDlkm(context) }
) {
Text(stringResource(R.string.unmount_vendor_dlkm))
}
}
AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) {
Column {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.mountVendorDlkm(context) }
) {
Text(stringResource(R.string.mount_vendor_dlkm))
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.unmapVendorDlkm(context) }
) {
Text(stringResource(R.string.unmap_vendor_dlkm))
}
}
}
AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.mapVendorDlkm(context) }
) {
Text(stringResource(R.string.map_vendor_dlkm))
}
}
}
}
}
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.slot
import android.provider.OpenableColumns
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.material3.AlertDialog
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.ExperimentalUnitApi
import androidx.compose.ui.unit.dp
import androidx.compose.ui.text.font.FontWeight
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.R
import com.github.capntrips.kernelflasher.common.PartitionUtil
import com.github.capntrips.kernelflasher.ui.components.DataCard
import com.github.capntrips.kernelflasher.ui.components.FlashButton
import com.github.capntrips.kernelflasher.ui.components.FlashList
import com.github.capntrips.kernelflasher.ui.components.SlotCard
import com.github.capntrips.kernelflasher.ui.components.DialogButton
import kotlinx.serialization.ExperimentalSerializationApi
import java.io.File
@ExperimentalAnimationApi
@ExperimentalMaterialApi
@ExperimentalMaterial3Api
@ExperimentalUnitApi
@ExperimentalSerializationApi
@Composable
fun ColumnScope.SlotFlashContent(
viewModel: SlotViewModel,
slotSuffix: String,
navController: NavController
) {
val context = LocalContext.current
val isRefreshing by remember { derivedStateOf { viewModel.isRefreshing } }
val currentRoute = navController.currentDestination?.route.orEmpty()
val isAk3 = currentRoute.contains("ak3")
val isFlashImage = currentRoute.endsWith("/flash/image")
val isBackup = currentRoute.endsWith("/backup")
val isBackupResult = currentRoute.endsWith("/backup/backup")
val isFlashAk3 = currentRoute.endsWith("/flash/ak3")
val isImageFlashResult = currentRoute.endsWith("/flash/image/flash")
val isFlashScreen = currentRoute.endsWith("/flash")
val isSlotScreen = !(isFlashAk3 || isImageFlashResult || isBackupResult) // Not in Flashing Screen; So Its considered Slot Screen
BackHandler(enabled = ((isFlashAk3 || isImageFlashResult || isBackupResult) && isRefreshing.value)) { }
if (isSlotScreen) {
SlotCard(
title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot),
viewModel = viewModel,
navController = navController,
isSlotScreen = true,
showDlkm = false
)
Spacer(Modifier.height(16.dp))
if (isFlashScreen) {
DataCard (stringResource(R.string.flash))
Spacer(Modifier.height(5.dp))
FlashButton(stringResource(R.string.flash_ak3_zip), "zip" ,callback = { uri ->
viewModel.flashActionType = "flashAk3"
viewModel.flashActionURI = uri
viewModel.showConfirmDialog()
})
FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip" ,callback = { uri ->
viewModel.flashActionType = "flashAk3_mkbootfs"
viewModel.flashActionURI = uri
viewModel.showConfirmDialog()
})
FlashButton(stringResource(R.string.flash_ksu_lkm), "ko" ,callback = { uri ->
viewModel.flashActionType = "flashKsuDriver"
viewModel.flashActionURI = uri
viewModel.showConfirmDialog()
})
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
navController.navigate("slot$slotSuffix/flash/image")
}
) {
Text(stringResource(R.string.flash_partition_image))
}
} else if (isFlashImage) {
DataCard (stringResource(R.string.flash_partition_image))
Spacer(Modifier.height(5.dp))
for (partitionName in PartitionUtil.AvailablePartitions) {
FlashButton(partitionName, "img" ,callback = { uri ->
viewModel.flashActionType = "flashImage"
viewModel.flashActionURI = uri
viewModel.flashActionPartName = partitionName
viewModel.showConfirmDialog()
})
}
} else if (isBackup) {
DataCard (stringResource(R.string.backup))
Spacer(Modifier.height(5.dp))
val disabledColor = ButtonDefaults.buttonColors(
Color.Transparent,
MaterialTheme.colorScheme.onSurface
)
for (partitionName in PartitionUtil.AvailablePartitions) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth()
.alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f),
shape = RoundedCornerShape(4.dp),
colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor,
onClick = {
viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!!
},
) {
Box(Modifier.fillMaxWidth()) {
Checkbox(viewModel.backupPartitions[partitionName]!!, null,
Modifier
.align(Alignment.CenterStart)
.offset(x = -(16.dp)))
Text(partitionName, Modifier.align(Alignment.Center))
}
}
}
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
viewModel.backup(context)
navController.navigate("slot$slotSuffix/backup/backup") {
popUpTo("slot$slotSuffix")
}
},
enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty()
) {
Text(stringResource(R.string.backup_now))
}
}
} else {
Text("")
FlashList(
stringResource(if (isBackupResult) R.string.backup else R.string.flash),
if (isAk3)
viewModel.uiPrintedOutput
else viewModel.flashOutput
) {
AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.wasFlashSuccess.value != null) {
Column {
if (isAk3) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { viewModel.saveLog(context) }
) {
Text(stringResource(R.string.save_ak3_log))
}
}
if (isAk3) {
AnimatedVisibility(!currentRoute.endsWith("/backups/{backupId}/flash/ak3") && viewModel.wasFlashSuccess.value != false) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = {
viewModel.backupZip(context) {
navController.navigate("slot$slotSuffix/backups") {
popUpTo("slot$slotSuffix")
}
}
}
) {
Text(stringResource(R.string.save_ak3_zip_as_backup))
}
}
}
if (viewModel.wasFlashSuccess.value == true && viewModel.showCautionDialog == true){
AlertDialog(
onDismissRequest = { viewModel.hideCautionDialog() },
title = { Text("CAUTION!", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("You have flashed to inactive slot!", fontWeight = FontWeight.Bold)
Text("But the active slot is not changed after flashing.", fontWeight = FontWeight.Bold)
Text("Change active slot or return to System Updater to complete OTA.", fontWeight = FontWeight.Bold)
Text("Do not reboot from here, unless you know what you are doing.", fontWeight = FontWeight.Bold)
}
},
confirmButton = {
DialogButton(
"CHANGE SLOT"
) {
viewModel.hideCautionDialog()
viewModel.switchSlot(context)
}
},
dismissButton = {
DialogButton(
"CANCEL"
) {
viewModel.hideCautionDialog()
}
},
modifier = Modifier.padding(16.dp)
)
}
if (viewModel.wasFlashSuccess.value != false && isBackupResult) {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { navController.popBackStack() }
) {
Text(stringResource(R.string.back))
}
} else {
OutlinedButton(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
onClick = { navController.navigate("reboot") }
) {
Text(stringResource(R.string.reboot))
}
}
}
}
}
}
if(viewModel.showConfirmDialog == true)
{
var filename = when {
viewModel.flashActionURI?.scheme == "file" -> {
File(viewModel.flashActionURI?.path ?: "").name
}
viewModel.flashActionURI != null -> {
context.contentResolver.query(viewModel.flashActionURI!!, null, null, null, null)?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
if (cursor.moveToFirst() && nameIndex != -1) {
cursor.getString(nameIndex)
} else null
} ?: "Unable to determine filename!"
}
else -> "Unable to determine filename!"
}
AlertDialog(
onDismissRequest = { viewModel.hideConfirmDialog() },
title = { Text("CAUTION!", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Are you Sure you want to flash this file?", fontWeight = FontWeight.Bold)
Text("", fontWeight = FontWeight.Bold)
Text("$filename", fontWeight = FontWeight.Bold)
}
},
confirmButton = {
DialogButton(
"Flash"
) {
viewModel.hideConfirmDialog()
val isOtherFlash =
viewModel.flashActionType != "flashImage" && viewModel.flashActionURI != null
val isPartitionFlash =
viewModel.flashActionType == "flashImage" && viewModel.flashActionPartName != null && viewModel.flashActionURI != null
if (isOtherFlash || isPartitionFlash) {
val uri = viewModel.flashActionURI!!
val partitionName: String? = viewModel.flashActionPartName
when (viewModel.flashActionType) {
"flashAk3" -> {
navController.navigate("slot$slotSuffix/flash/ak3") {
popUpTo("slot$slotSuffix")
}
viewModel.flashAk3(context, uri)
}
"flashAk3_mkbootfs" -> {
navController.navigate("slot$slotSuffix/flash/ak3") {
popUpTo("slot$slotSuffix")
}
viewModel.flashAk3_mkbootfs(context, uri)
}
"flashKsuDriver" -> {
navController.navigate("slot$slotSuffix/flash/image/flash") {
popUpTo("slot$slotSuffix")
}
viewModel.flashKsuDriver(context, uri)
}
"flashImage" -> {
navController.navigate("slot$slotSuffix/flash/image/flash") {
popUpTo("slot$slotSuffix")
}
viewModel.flashImage(
context,
uri,
partitionName!!
)
}
}
viewModel.flashActionType = ""
viewModel.flashActionURI = null
viewModel.flashActionPartName = null
}
}
},
dismissButton = {
DialogButton(
"CANCEL"
) {
viewModel.hideConfirmDialog()
}
},
modifier = Modifier.padding(16.dp)
)
}
}
================================================
FILE: app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt
================================================
package com.github.capntrips.kernelflasher.ui.screens.slot
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.NavController
import com.github.capntrips.kernelflasher.SharedViewModels
import com.github.capntrips.kernelflasher.common.PartitionUtil
import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex
import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.inputStream
import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream
import com.github.capntrips.kernelflasher.common.types.backups.Backup
import com.github.capntrips.kernelflasher.common.types.partitions.Partitions
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.nio.ExtendedFile
import com.topjohnwu.superuser.nio.FileSystemManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import java.io.File
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.zip.ZipFile
class SlotViewModel(
context: Context,
private val fileSystemManager: FileSystemManager,
private val navController: NavController,
private val _isRefreshing: MutableState<Boolean>,
val isActive: Boolean,
val slotSuffix: String,
val boot: File,
val initBoot: File?,
private val _backups: MutableMap<String, Backup>
) : ViewModel() {
companion object {
const val TAG: String = "KernelFlasher/SlotState"
const val HEADER_VER = "HEADER_VER"
const val KERNEL_FMT = "KERNEL_FMT"
const val RAMDISK_FMT = "RAMDISK_FMT"
const val VND_RAMDISK = "VND_RAMDISK"
}
data class BootSlotInfo(
var unbootable: String? = null,
var successful: String? = null,
)
data class BootImgInfo(
var kernelVersion: String? = null,
var bootFmt: String? = null,
var headerVersion: String? = null,
)
data class RamdiskInfo(
var headerVersion: String? = null,
var ramdiskFmt: String? = null,
var ramdiskLocation: String? = null,
)
data class SlotInfo(
var bootSlotInfo: BootSlotInfo,
var bootImgInfo: BootImgInfo,
var ramdiskInfo: RamdiskInfo,
)
private var _sha1: String? = null
private val _slotInfo: MutableState<SlotInfo> = mutableStateOf(SlotInfo(BootSlotInfo(), BootImgInfo(), RamdiskInfo()))
var hasVendorDlkm: Boolean = false
var isVendorDlkmMapped: Boolean = false
var isVendorDlkmMounted: Boolean = false
private val _flashOutput: SnapshotStateList<String> = mutableStateListOf()
private val _wasFlashSuccess: MutableState<Boolean?> = mutableStateOf(null)
private val _backupPartitions: SnapshotStateMap<String, Boolean> = mutableStateMapOf()
private var wasSlotReset: Boolean = false
private var flashUri: Uri? = null
private var flashFilename: String? = null
private val hashAlgorithm: String = "SHA-256"
private var inInit = true
private var _error: String? = null
private val _showCautionDialog: MutableState<Boolean> = mutableStateOf(false)
private val _showConfirmDialog: MutableState<Boolean> = mutableStateOf(false)
var flashActionType: String = ""
var flashActionURI: Uri? = null
var flashActionPartName: String? = null
val sha1: String?
get() = _sha1
val flashOutput: List<String>
get() = _flashOutput
val uiPrintedOutput: List<String>
get() = _flashOutput.filter { it.startsWith("ui_print") }.map { it.substringAfter("ui_print").trim() }.filter { it.isNotEmpty() || it == "" }
val wasFlashSuccess: MutableState<Boolean?>
get() = _wasFlashSuccess
val backupPartitions: MutableMap<String, Boolean>
get() = _backupPartitions
val isRefreshing: MutableState<Boolean>
get() = _isRefreshing
val hasError: Boolean
get() = _error != null
val error: String?
get() = _error
val showCautionDialog: Boolean
get() = _showCautionDialog.value
val showConfirmDialog: Boolean
get() = _showConfirmDialog.value
val slotInfo: SlotInfo
get() = _slotInfo.value
init {
refresh(context)
}
private fun extractKernelValues(input: String, key: String, isVendor_boot: Boolean = false): String? {
val regex = if(isVendor_boot == true) Regex("VND_RAMDISK.*fmt=\\[([^]]+)]") else Regex("$key\\s*\\[([^]]+)]")
return regex.find(input)?.groupValues?.get(1)
}
fun refresh(context: Context) {
_error = null
_sha1 = null
_slotInfo.value.bootSlotInfo = _slotInfo.value.bootSlotInfo.copy(null, null)
_slotInfo.value.bootImgInfo = _slotInfo.value.bootImgInfo.copy(null, null, null)
_slotInfo.value.ramdiskInfo = _slotInfo.value.ramdiskInfo.copy(null, null, null)
if (!isActive) {
inInit = true
}
val magiskboot = File(context.filesDir, "magiskboot")
val bootctl = File(context.filesDir, "bootctl")
Shell.cmd("$magiskboot cleanup").exec()
val unpackBootOutput = mutableListOf<String>()
Shell.cmd("$magiskboot unpack $boot").to(unpackBootOutput, unpackBootOutput).exec()
val bootUnpackOp = unpackBootOutput.joinToString("\n")
if(slotSuffix != "") {
val resCode1 = Shell.cmd("$bootctl is-slot-bootable " + if (slotSuffix == "_a") "0" else "1").exec().code
_slotInfo.value.bootSlotInfo.unbootable = if(resCode1 == 0) "No" else "Yes"
val resCode2 = Shell.cmd("$bootctl is-slot-marked-successful " + if (slotSuffix == "_a") "0" else "1").exec().code
_slotInfo.value.bootSlotInfo.successful = if(resCode2 == 0) "Yes" else "No"
}
_slotInfo.value.bootImgInfo.headerVersion = extractKernelValues(bootUnpackOp.trimIndent(), HEADER_VER)
_slotInfo.value.bootImgInfo.bootFmt = extractKernelValues(bootUnpackOp.trimIndent(), KERNEL_FMT)
_slotInfo.value.ramdiskInfo.ramdiskFmt = extractKernelValues(bootUnpackOp.trimIndent(), RAMDISK_FMT)
if (_slotInfo.value.ramdiskInfo.ramdiskFmt != null)
{
_slotInfo.value.ramdiskInfo.ramdiskLocation = "boot.img"
_slotInfo.value.ramdiskInfo.headerVersion = _slotInfo.value.bootImgInfo.headerVersion
}
Log.d(TAG, _slotInfo.value.bootImgInfo.toString())
if (initBoot != null && _slotInfo.value.ramdiskInfo.ramdiskFmt == null) {
val unpackInitBootOutput = mutableListOf<String>()
if(Shell.cmd("$magiskboot unpack $initBoot").to(unpackInitBootOutput, unpackInitBootOutput).exec().isSuccess)
{
val initBootUnpackOp = unpackInitBootOutput.joinToString("\n")
_slotInfo.value.ramdiskInfo.ramdiskFmt = extractKernelValues(initBootUnpackOp.trimIndent(), RAMDISK_FMT)
_slotInfo.value.ramdiskInfo.ramdiskLocation = "init_bo
gitextract_gnutdixd/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── build.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── proguard-rules.pro │ ├── schemas/ │ │ └── com.github.capntrips.kernelflasher.common.types.room.AppDatabase/ │ │ └── 1.json │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── aidl/ │ │ └── com/ │ │ └── github/ │ │ └── capntrips/ │ │ └── kernelflasher/ │ │ └── IFilesystemService.aidl │ ├── assets/ │ │ ├── flash_ak3.sh │ │ ├── flash_ak3_mkbootfs.sh │ │ ├── ksuinit │ │ └── mkbootfs │ ├── java/ │ │ └── com/ │ │ └── github/ │ │ └── capntrips/ │ │ └── kernelflasher/ │ │ ├── AppUpdater.kt │ │ ├── FilesystemService.kt │ │ ├── MainActivity.kt │ │ ├── MainListener.kt │ │ ├── common/ │ │ │ ├── PartitionUtil.kt │ │ │ ├── extensions/ │ │ │ │ ├── ByteArray.kt │ │ │ │ └── ExtendedFile.kt │ │ │ └── types/ │ │ │ ├── backups/ │ │ │ │ └── Backup.kt │ │ │ ├── partitions/ │ │ │ │ ├── FsMgrFlags.kt │ │ │ │ ├── FstabEntry.kt │ │ │ │ └── Partitions.kt │ │ │ └── room/ │ │ │ ├── AppDatabase.kt │ │ │ ├── Converters.kt │ │ │ └── updates/ │ │ │ ├── Update.kt │ │ │ └── UpdateDao.kt │ │ └── ui/ │ │ ├── components/ │ │ │ ├── Card.kt │ │ │ ├── DataCard.kt │ │ │ ├── DataRow.kt │ │ │ ├── DataSet.kt │ │ │ ├── DataValue.kt │ │ │ ├── DialogButton.kt │ │ │ ├── FlashButton.kt │ │ │ ├── FlashList.kt │ │ │ ├── SlotCard.kt │ │ │ └── ViewButton.kt │ │ ├── screens/ │ │ │ ├── RefreshableScreen.kt │ │ │ ├── backups/ │ │ │ │ ├── BackupsContent.kt │ │ │ │ ├── BackupsViewModel.kt │ │ │ │ └── SlotBackupsContent.kt │ │ │ ├── error/ │ │ │ │ └── ErrorScreen.kt │ │ │ ├── main/ │ │ │ │ ├── MainContent.kt │ │ │ │ └── MainViewModel.kt │ │ │ ├── reboot/ │ │ │ │ ├── RebootContent.kt │ │ │ │ └── RebootViewModel.kt │ │ │ ├── slot/ │ │ │ │ ├── SlotContent.kt │ │ │ │ ├── SlotFlashContent.kt │ │ │ │ └── SlotViewModel.kt │ │ │ └── updates/ │ │ │ ├── UpdatesAddContent.kt │ │ │ ├── UpdatesChangelogContent.kt │ │ │ ├── UpdatesContent.kt │ │ │ ├── UpdatesUrlState.kt │ │ │ ├── UpdatesViewContent.kt │ │ │ └── UpdatesViewModel.kt │ │ └── theme/ │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res/ │ ├── drawable/ │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_splash_animation.xml │ │ └── ic_splash_foreground.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── resources.properties │ ├── values/ │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-night/ │ │ └── themes.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt-rBR/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle.kts ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle.kts
Condensed preview — 90 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (317K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 143,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"gradle\"\n directory: \"/\"\n target-branch: \"allow-errors\"\n schedule:\n "
},
{
"path": ".github/workflows/build.yml",
"chars": 4445,
"preview": "name: Android Build\npermissions:\n contents: write\non:\n workflow_dispatch:\n push:\n pull_request:\n\njobs:\n build:\n "
},
{
"path": ".github/workflows/publish.yml",
"chars": 5298,
"preview": "name: Android Release\npermissions:\n contents: write\non:\n workflow_dispatch:\n inputs:\n increment_major:\n "
},
{
"path": ".gitignore",
"chars": 200,
"preview": "*.iml\n.gradle\n/local.properties\n/.idea\n.DS_Store\n/app/release\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.propertie"
},
{
"path": "LICENSE",
"chars": 2618,
"preview": "Copyright (c) 2022 capntrips\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this softw"
},
{
"path": "README.md",
"chars": 636,
"preview": "[](https://GitHub.com/fatalcoder524/"
},
{
"path": "app/.gitignore",
"chars": 41,
"preview": "/build\r\n/release\r\nbuild.gradle.bak\r\n*.bak"
},
{
"path": "app/build.gradle.kts",
"chars": 3409,
"preview": "plugins {\n alias(libs.plugins.android.application)\n alias(libs.plugins.devtools.ksp)\n alias(libs.plugins.kotlin"
},
{
"path": "app/proguard-rules.pro",
"chars": 2012,
"preview": "# GENERAL\n-dontobfuscate\n-keepattributes Signature\n-keepattributes RuntimeVisibleAnnotations\n\n# ANDROID INTERFACES / AID"
},
{
"path": "app/schemas/com.github.capntrips.kernelflasher.common.types.room.AppDatabase/1.json",
"chars": 2490,
"preview": "{\n \"formatVersion\": 1,\n \"database\": {\n \"version\": 1,\n \"identityHash\": \"bbe3033de836fa33fb2ed46b5272124e\",\n \"e"
},
{
"path": "app/src/main/AndroidManifest.xml",
"chars": 2279,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n xmlns:to"
},
{
"path": "app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl",
"chars": 113,
"preview": "package com.github.capntrips.kernelflasher;\n\ninterface IFilesystemService {\n IBinder getFileSystemService();\n}"
},
{
"path": "app/src/main/assets/flash_ak3.sh",
"chars": 1157,
"preview": "#!/system/bin/sh\n\n## setup for testing:\nunzip -p \"$Z\" tools*/busybox > $F/busybox_ak;\nunzip -p \"$Z\" META-INF/com/google/"
},
{
"path": "app/src/main/assets/flash_ak3_mkbootfs.sh",
"chars": 1237,
"preview": "#!/system/bin/sh\n\n## setup for testing:\nunzip -p \"$Z\" tools*/busybox > $F/busybox_ak;\nunzip -p \"$Z\" META-INF/com/google/"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/AppUpdater.kt",
"chars": 5427,
"preview": "package com.github.capntrips.kernelflasher\r\n\r\nimport android.annotation.SuppressLint\r\nimport android.app.DownloadManager"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt",
"chars": 514,
"preview": "package com.github.capntrips.kernelflasher\n\nimport android.content.Intent\nimport android.os.IBinder\nimport com.topjohnwu"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/MainActivity.kt",
"chars": 34776,
"preview": "package com.github.capntrips.kernelflasher\n\nimport android.animation.ObjectAnimator\nimport android.animation.PropertyVal"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt",
"chars": 161,
"preview": "package com.github.capntrips.kernelflasher\n\ninternal class MainListener(private val callback: () -> Unit) {\n fun resu"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt",
"chars": 8320,
"preview": "package com.github.capntrips.kernelflasher.common\n\nimport android.content.Context\nimport com.github.capntrips.kernelflas"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt",
"chars": 195,
"preview": "package com.github.capntrips.kernelflasher.common.extensions\n\nimport kotlin.ByteArray\n\nobject ByteArray {\n fun ByteAr"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt",
"chars": 894,
"preview": "package com.github.capntrips.kernelflasher.common.extensions\n\nimport com.topjohnwu.superuser.nio.ExtendedFile\nimport jav"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt",
"chars": 437,
"preview": "package com.github.capntrips.kernelflasher.common.types.backups\n\nimport com.github.capntrips.kernelflasher.common.types."
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt",
"chars": 183,
"preview": "package com.github.capntrips.kernelflasher.common.types.partitions\n\nimport kotlinx.serialization.Serializable\n\n@Serializ"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt",
"chars": 418,
"preview": "package com.github.capntrips.kernelflasher.common.types.partitions\n\nimport kotlinx.serialization.Serializable\n\n@Serializ"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt",
"chars": 1662,
"preview": "package com.github.capntrips.kernelflasher.common.types.partitions\n\nimport kotlinx.serialization.Serializable\n\n@Serializ"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt",
"chars": 490,
"preview": "package com.github.capntrips.kernelflasher.common.types.room\n\nimport androidx.room.Database\nimport androidx.room.RoomDat"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt",
"chars": 348,
"preview": "package com.github.capntrips.kernelflasher.common.types.room\n\nimport androidx.room.TypeConverter\nimport java.util.Date\n\n"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt",
"chars": 3581,
"preview": "package com.github.capntrips.kernelflasher.common.types.room.updates\n\nimport androidx.room.ColumnInfo\nimport androidx.ro"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt",
"chars": 522,
"preview": "package com.github.capntrips.kernelflasher.common.types.room.updates\n\nimport androidx.room.Dao\nimport androidx.room.Dele"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt",
"chars": 1567,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.BorderStroke\nimport android"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt",
"chars": 1520,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.Arrangement\nimport a"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt",
"chars": 1870,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.Row\nimport androidx."
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt",
"chars": 869,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.Column\nimport androi"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataValue.kt",
"chars": 1511,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.clickable\nimport androidx.c"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt",
"chars": 1119,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt",
"chars": 2867,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport android.net.Uri\nimport android.provider.OpenableColumns"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt",
"chars": 4954,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.animation.core.animateFloatAsState\nimp"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt",
"chars": 4814,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.animation.AnimatedVisibility\nimport an"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt",
"chars": 1106,
"preview": "package com.github.capntrips.kernelflasher.ui.components\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt",
"chars": 4895,
"preview": "package com.github.capntrips.kernelflasher.ui.screens\n\nimport androidx.compose.animation.AnimatedVisibility\nimport andro"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt",
"chars": 6571,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.backups\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt",
"chars": 12335,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.backups\n\nimport android.annotation.SuppressLint\nimport android.con"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt",
"chars": 13102,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.backups\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt",
"chars": 1876,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.error\n\nimport androidx.compose.foundation.layout.Box\nimport androi"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt",
"chars": 4551,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.main\n\nimport android.os.Build\nimport androidx.compose.animation.An"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt",
"chars": 8488,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.main\n\nimport android.annotation.SuppressLint\nimport android.conten"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt",
"chars": 2064,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.reboot\n\nimport android.os.Build\nimport android.os.PowerManager\nimp"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt",
"chars": 2236,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.reboot\n\nimport android.content.Context\nimport android.util.Log\nimp"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt",
"chars": 5365,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.slot\n\nimport androidx.compose.animation.AnimatedVisibility\nimport "
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt",
"chars": 15768,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.slot\n\nimport android.provider.OpenableColumns\nimport androidx.acti"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt",
"chars": 37634,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.slot\n\nimport android.annotation.SuppressLint\nimport android.conten"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt",
"chars": 1899,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.foundation.layout.ColumnScope\nimp"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt",
"chars": 1526,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.foundation.layout.ColumnScope\nimp"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt",
"chars": 3802,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesUrlState.kt",
"chars": 144,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\n@Suppress(\"unused\")\nclass UpdatesUrlState {\n // TODO: "
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt",
"chars": 4283,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport androidx.compose.animation.AnimatedVisibility\nimpo"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewModel.kt",
"chars": 7833,
"preview": "package com.github.capntrips.kernelflasher.ui.screens.updates\n\nimport android.content.ContentValues\nimport android.conte"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt",
"chars": 130,
"preview": "package com.github.capntrips.kernelflasher.ui.theme\n\nimport androidx.compose.ui.graphics.Color\n\nval Orange500 = Color(0x"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt",
"chars": 1106,
"preview": "package com.github.capntrips.kernelflasher.ui.theme\n\nimport android.os.Build\nimport androidx.compose.foundation.isSystem"
},
{
"path": "app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Type.kt",
"chars": 135,
"preview": "package com.github.capntrips.kernelflasher.ui.theme\n\nimport androidx.compose.material3.Typography\n\nval Typography = Typo"
},
{
"path": "app/src/main/res/drawable/ic_launcher_background.xml",
"chars": 330,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable/ic_launcher_foreground.xml",
"chars": 900,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/drawable/ic_splash_animation.xml",
"chars": 1175,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<animated-vector\n xmlns:android=\"http://schemas.android.com/apk/res/android\"\n "
},
{
"path": "app/src/main/res/drawable/ic_splash_foreground.xml",
"chars": 1623,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n android:wi"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
"chars": 344,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
"chars": 344,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n <b"
},
{
"path": "app/src/main/res/resources.properties",
"chars": 26,
"preview": "unqualifiedResLocale=en-US"
},
{
"path": "app/src/main/res/values/colors.xml",
"chars": 118,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <color name=\"ic_splash_background\">#000000</color>\n</resources>"
},
{
"path": "app/src/main/res/values/strings.xml",
"chars": 4163,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <string name=\"app_name\">Kernel Flasher</string>\n <stri"
},
{
"path": "app/src/main/res/values/themes.xml",
"chars": 802,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"Theme.KernelFlasher\" parent=\"Theme.Material3.DayNigh"
},
{
"path": "app/src/main/res/values-it/strings.xml",
"chars": 4296,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <string name=\"app_name\">Kernel Flasher</string>\n <stri"
},
{
"path": "app/src/main/res/values-ja/strings.xml",
"chars": 3705,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <string name=\"app_n"
},
{
"path": "app/src/main/res/values-night/themes.xml",
"chars": 400,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <style name=\"Theme.KernelFlasher\" parent=\"Theme.Material3.DayNigh"
},
{
"path": "app/src/main/res/values-pl/strings.xml",
"chars": 4223,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\">Kernel Flasher</string>\n <string name="
},
{
"path": "app/src/main/res/values-pt-rBR/strings.xml",
"chars": 4132,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n <string name=\"app_name\">Kernel Flasher</string>\r\n <string na"
},
{
"path": "app/src/main/res/values-ru/strings.xml",
"chars": 4220,
"preview": "<resources xmlns:tools=\"http://schemas.android.com/tools\">\n <string name=\"app_name\">Kernel Flasher</string>\n <stri"
},
{
"path": "app/src/main/res/values-zh-rCN/strings.xml",
"chars": 3412,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\">Kernel Flasher</string>\n <string name="
},
{
"path": "app/src/main/res/values-zh-rTW/strings.xml",
"chars": 3418,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n <string name=\"app_name\">Kernel Flasher</string>\n <string name="
},
{
"path": "app/src/main/res/xml/backup_rules.xml",
"chars": 478,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample backup rules file; uncomment and customize as necessary.\n See htt"
},
{
"path": "app/src/main/res/xml/data_extraction_rules.xml",
"chars": 551,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n Sample data extraction rules file; uncomment and customize as necessary.\n "
},
{
"path": "build.gradle.kts",
"chars": 370,
"preview": "plugins {\n alias(libs.plugins.android.application) apply false\n alias(libs.plugins.devtools.ksp) apply false\n a"
},
{
"path": "gradle/libs.versions.toml",
"chars": 3420,
"preview": "[versions]\nkotlin = \"2.3.0\"\n\nandroidx-activity-compose = \"1.12.2\"\nandroidx-appcompat = \"1.7.1\"\nandroidx-compose = \"1.10."
},
{
"path": "gradle/wrapper/gradle-wrapper.properties",
"chars": 233,
"preview": "#Fri Apr 14 13:36:42 CDT 2023\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://"
},
{
"path": "gradle.properties",
"chars": 1407,
"preview": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will ov"
},
{
"path": "gradlew",
"chars": 5766,
"preview": "#!/usr/bin/env sh\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0"
},
{
"path": "gradlew.bat",
"chars": 2674,
"preview": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \""
},
{
"path": "settings.gradle.kts",
"chars": 401,
"preview": "pluginManagement {\n repositories {\n gradlePluginPortal()\n google()\n mavenCentral()\n }\n}\ndepen"
}
]
// ... and 3 more files (download for full content)
About this extraction
This page contains the full source code of the fatalcoder524/KernelFlasher GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 90 files (288.0 KB), approximately 68.2k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.