├── app
├── .gitignore
├── proguard-rules.pro
├── src
│ └── main
│ │ ├── assets
│ │ ├── ksuinit
│ │ ├── mkbootfs
│ │ ├── flash_ak3.sh
│ │ └── flash_ak3_mkbootfs.sh
│ │ ├── jniLibs
│ │ ├── arm64-v8a
│ │ │ ├── libmagiskboot.so
│ │ │ ├── libhttools_static.so
│ │ │ └── liblptools_static.so
│ │ └── armeabi-v7a
│ │ │ ├── libmagiskboot.so
│ │ │ ├── libhttools_static.so
│ │ │ └── liblptools_static.so
│ │ ├── res
│ │ ├── values
│ │ │ ├── colors.xml
│ │ │ ├── themes.xml
│ │ │ └── strings.xml
│ │ ├── values-night
│ │ │ └── themes.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── drawable
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── ic_splash_animation.xml
│ │ │ └── ic_splash_foreground.xml
│ │ ├── xml
│ │ │ ├── backup_rules.xml
│ │ │ └── data_extraction_rules.xml
│ │ ├── values-zh-rTW
│ │ │ └── strings.xml
│ │ ├── values-zh-rCN
│ │ │ └── strings.xml
│ │ ├── values-ja
│ │ │ └── strings.xml
│ │ ├── values-pt-rBR
│ │ │ └── strings.xml
│ │ ├── values-ru
│ │ │ └── string.xml
│ │ └── values-pl
│ │ │ └── strings.xml
│ │ ├── aidl
│ │ └── com
│ │ │ └── github
│ │ │ └── capntrips
│ │ │ └── kernelflasher
│ │ │ └── IFilesystemService.aidl
│ │ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── capntrips
│ │ │ └── kernelflasher
│ │ │ ├── ui
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ ├── screens
│ │ │ │ ├── updates
│ │ │ │ │ ├── UpdatesUrlState.kt
│ │ │ │ │ ├── UpdatesChangelogContent.kt
│ │ │ │ │ ├── UpdatesAddContent.kt
│ │ │ │ │ ├── UpdatesContent.kt
│ │ │ │ │ ├── UpdatesViewContent.kt
│ │ │ │ │ └── UpdatesViewModel.kt
│ │ │ │ ├── reboot
│ │ │ │ │ ├── RebootContent.kt
│ │ │ │ │ └── RebootViewModel.kt
│ │ │ │ ├── error
│ │ │ │ │ └── ErrorScreen.kt
│ │ │ │ ├── slot
│ │ │ │ │ ├── SlotContent.kt
│ │ │ │ │ └── SlotFlashContent.kt
│ │ │ │ ├── main
│ │ │ │ │ ├── MainContent.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ ├── RefreshableScreen.kt
│ │ │ │ └── backups
│ │ │ │ │ ├── BackupsContent.kt
│ │ │ │ │ ├── SlotBackupsContent.kt
│ │ │ │ │ └── BackupsViewModel.kt
│ │ │ └── components
│ │ │ │ ├── DataSet.kt
│ │ │ │ ├── ViewButton.kt
│ │ │ │ ├── DialogButton.kt
│ │ │ │ ├── MyOutlinedButton.kt
│ │ │ │ ├── DataValue.kt
│ │ │ │ ├── DataRow.kt
│ │ │ │ ├── DataCard.kt
│ │ │ │ ├── FlashButton.kt
│ │ │ │ ├── SlotCard.kt
│ │ │ │ └── FlashList.kt
│ │ │ ├── MainListener.kt
│ │ │ ├── common
│ │ │ ├── types
│ │ │ │ ├── partitions
│ │ │ │ │ ├── FsMgrFlags.kt
│ │ │ │ │ ├── FstabEntry.kt
│ │ │ │ │ └── Partitions.kt
│ │ │ │ ├── room
│ │ │ │ │ ├── Converters.kt
│ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ └── updates
│ │ │ │ │ │ ├── UpdateDao.kt
│ │ │ │ │ │ └── Update.kt
│ │ │ │ └── backups
│ │ │ │ │ └── Backup.kt
│ │ │ ├── extensions
│ │ │ │ ├── ByteArray.kt
│ │ │ │ └── ExtendedFile.kt
│ │ │ └── PartitionUtil.kt
│ │ │ └── FilesystemService.kt
│ │ └── AndroidManifest.xml
└── build.gradle
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .github
├── dependabot.yml
└── workflows
│ ├── build.yml
│ └── publish.yml
├── .gitignore
├── README.md
├── settings.gradle
├── gradle.properties
├── LICENSE
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | -repackageclasses
2 | -allowaccessmodification
3 | -overloadaggressively
--------------------------------------------------------------------------------
/app/src/main/assets/ksuinit:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/assets/ksuinit
--------------------------------------------------------------------------------
/app/src/main/assets/mkbootfs:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/assets/mkbootfs
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "gradle"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libhttools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/jniLibs/arm64-v8a/libhttools_static.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/liblptools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/jniLibs/arm64-v8a/liblptools_static.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libhttools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/jniLibs/armeabi-v7a/libhttools_static.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/liblptools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/qlenlen/KernelFlasher/HEAD/app/src/main/jniLibs/armeabi-v7a/liblptools_static.so
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #000000
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /app/release
7 | /build
8 | /captures
9 | .externalNativeBuild
10 | .cxx
11 | local.properties
12 |
--------------------------------------------------------------------------------
/app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher;
2 |
3 | interface IFilesystemService {
4 | IBinder getFileSystemService();
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Orange500 = Color(0xFFFF9800)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | val Typography = Typography().copy()
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher
2 |
3 | internal class MainListener(private val callback: () -> Unit) {
4 | fun resume() {
5 | callback.invoke()
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesUrlState.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.updates
2 |
3 | @Suppress("unused")
4 | class UpdatesUrlState {
5 | // TODO: validate the url field
6 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 14 13:36:42 CDT 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.partitions
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class FsMgrFlags(
7 | val logical: Boolean = false
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.extensions
2 |
3 | import kotlin.ByteArray
4 |
5 | object ByteArray {
6 | fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) }
7 | }
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.room
2 |
3 | import androidx.room.TypeConverter
4 | import java.util.Date
5 |
6 | class Converters {
7 | @TypeConverter
8 | fun fromTimestamp(value: Long?): Date? {
9 | return value?.let { Date(it) }
10 | }
11 |
12 | @TypeConverter
13 | fun dateToTimestamp(date: Date?): Long? {
14 | return date?.time
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.partitions
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class FstabEntry(
7 | val blkDevice: String,
8 | val mountPoint: String,
9 | val fsType: String,
10 | val logicalPartitionName: String? = null,
11 | val avb: Boolean = false,
12 | val vbmetaPartition: String? = null,
13 | val avbKeys: String? = null,
14 | val fsMgrFlags: FsMgrFlags? = null
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.backups
2 |
3 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 | data class Backup(
8 | val name: String,
9 | val type: String,
10 | val kernelVersion: String,
11 | val bootSha1: String? = null,
12 | val filename: String? = null,
13 | val hashes: Partitions? = null,
14 | val hashAlgorithm: String? = null
15 | )
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.room
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import com.github.capntrips.kernelflasher.common.types.room.updates.Update
7 | import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateDao
8 |
9 | @Database(entities = [Update::class], version = 1)
10 | @TypeConverters(Converters::class)
11 | abstract class AppDatabase : RoomDatabase() {
12 | abstract fun updateDao(): UpdateDao
13 | }
14 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher
2 |
3 | import android.content.Intent
4 | import android.os.IBinder
5 | import com.topjohnwu.superuser.ipc.RootService
6 | import com.topjohnwu.superuser.nio.FileSystemManager
7 |
8 | class FilesystemService : RootService() {
9 | inner class FilesystemIPC : IFilesystemService.Stub() {
10 | override fun getFileSystemService(): IBinder {
11 | return FileSystemManager.getService()
12 | }
13 | }
14 |
15 | override fun onBind(intent: Intent): IBinder {
16 | return FilesystemIPC()
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://GitHub.com/qlenlen/KernelFlasher/releases/)
2 | [](https://GitHub.com/qlenlen/KernelFlasher/releases/)
3 |
4 | # Kernel Flasher
5 |
6 | Kernel Flasher is an Android app to flash, backup, and restore kernels.
7 |
8 | ## Usage
9 |
10 | `View` a slot and choose to `Flash` an AK3 zip, `Backup` the kernel related partitions, or `Restore` a previous backup.
11 |
12 | There are also options to toggle the mount and map status of `vendor_dlkm` and to save `dmesg` and `logcat`.
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.room.updates
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Delete
5 | import androidx.room.Insert
6 | import androidx.room.Query
7 |
8 | @Dao
9 | interface UpdateDao {
10 | @Query("""SELECT * FROM "update"""")
11 | fun getAll(): List
12 |
13 | @Query("""SELECT * FROM "update" WHERE id IN (:id)""")
14 | fun load(id: Int): Update
15 |
16 | @Insert
17 | fun insert(update: Update): Long
18 |
19 | @androidx.room.Update
20 | fun update(update: Update)
21 |
22 | @Delete
23 | fun delete(update: Update)
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
13 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | maven { setUrl("https://jitpack.io") }
4 | google()
5 | mavenCentral()
6 | gradlePluginPortal()
7 | }
8 | }
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | maven { setUrl("https://maven.aliyun.com/repository/central") }
13 | maven { setUrl("https://maven.aliyun.com/repository/jcenter") }
14 | maven { setUrl("https://maven.aliyun.com/repository/google") }
15 | maven { setUrl("https://maven.aliyun.com/repository/gradle-plugin") }
16 | maven { setUrl("https://maven.aliyun.com/repository/public") }
17 | maven { setUrl("https://jitpack.io") }
18 | google()
19 | mavenCentral()
20 | gradlePluginPortal()
21 | }
22 | }
23 | rootProject.name = "Kernel Flasher"
24 | include ':app'
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.layout.Column
4 | import androidx.compose.foundation.layout.ColumnScope
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.graphics.Color
11 | import androidx.compose.ui.text.TextStyle
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun DataSet(
16 | label: String,
17 | labelColor: Color = Color.Unspecified,
18 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium,
19 | content: @Composable (ColumnScope.() -> Unit)
20 | ) {
21 | Text(
22 | text = label,
23 | color = labelColor,
24 | style = labelStyle
25 | )
26 | Column(Modifier.padding(start = 16.dp)) {
27 | content()
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/assets/flash_ak3.sh:
--------------------------------------------------------------------------------
1 | #!/system/bin/sh
2 |
3 | ## setup for testing:
4 | unzip -p "$Z" tools*/busybox > $F/busybox;
5 | unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary;
6 | ##
7 |
8 | chmod 755 $F/busybox;
9 | $F/busybox chmod 755 $F/update-binary;
10 | $F/busybox chown root:root $F/busybox $F/update-binary;
11 |
12 | TMP=$F/tmp;
13 |
14 | $F/busybox umount $TMP 2>/dev/null;
15 | $F/busybox rm -rf $TMP 2>/dev/null;
16 | $F/busybox mkdir -p $TMP;
17 |
18 | $F/busybox mount -t tmpfs -o noatime tmpfs $TMP;
19 | $F/busybox mount | $F/busybox grep -q " $TMP " || exit 1;
20 |
21 | # update-binary
22 | AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z";
23 | RC=$?;
24 |
25 | $F/busybox umount $TMP;
26 | $F/busybox rm -rf $TMP;
27 | $F/busybox mount -o ro,remount -t auto /;
28 | $F/busybox rm -f $F/update-binary $F/busybox;
29 |
30 | # work around libsu not cleanly accepting return or exit as last line
31 | safereturn() { return $RC; }
32 | safereturn;
33 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.extensions
2 |
3 | import com.topjohnwu.superuser.nio.ExtendedFile
4 | import java.io.InputStream
5 | import java.io.InputStreamReader
6 | import java.io.OutputStream
7 | import java.nio.charset.Charset
8 |
9 | object ExtendedFile {
10 | private fun ExtendedFile.reader(charset: Charset = Charsets.UTF_8): InputStreamReader =
11 | inputStream().reader(charset)
12 |
13 | private fun ExtendedFile.writeBytes(array: kotlin.ByteArray): Unit =
14 | outputStream().use { it.write(array) }
15 |
16 | fun ExtendedFile.readText(charset: Charset = Charsets.UTF_8): String =
17 | reader(charset).use { it.readText() }
18 |
19 | @Suppress("unused")
20 | fun ExtendedFile.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit =
21 | writeBytes(text.toByteArray(charset))
22 |
23 | fun ExtendedFile.inputStream(): InputStream = newInputStream()
24 |
25 | fun ExtendedFile.outputStream(): OutputStream = newOutputStream()
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material3.MaterialTheme
5 | import androidx.compose.material3.darkColorScheme
6 | import androidx.compose.material3.dynamicDarkColorScheme
7 | import androidx.compose.material3.dynamicLightColorScheme
8 | import androidx.compose.material3.lightColorScheme
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.platform.LocalContext
11 |
12 | @Composable
13 | fun KernelFlasherTheme(
14 | darkTheme: Boolean = isSystemInDarkTheme(),
15 | dynamicColor: Boolean = true,
16 | content: @Composable () -> Unit
17 | ) {
18 | val colorScheme = when {
19 | dynamicColor -> {
20 | val context = LocalContext.current
21 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
22 | }
23 |
24 | darkTheme -> darkColorScheme()
25 | else -> lightColorScheme()
26 | }
27 | MaterialTheme(
28 | colorScheme = colorScheme,
29 | typography = Typography,
30 | content = content
31 | )
32 | }
--------------------------------------------------------------------------------
/app/src/main/assets/flash_ak3_mkbootfs.sh:
--------------------------------------------------------------------------------
1 | #!/system/bin/sh
2 |
3 | ## setup for testing:
4 | unzip -p "$Z" tools*/busybox > $F/busybox;
5 | unzip -p "$Z" META-INF/com/google/android/update-binary > $F/update-binary;
6 | ##
7 |
8 | chmod 755 $F/busybox;
9 | $F/busybox chmod 755 $F/update-binary;
10 | $F/busybox chown root:root $F/busybox $F/update-binary;
11 |
12 | TMP=$F/tmp;
13 |
14 | $F/busybox umount $TMP 2>/dev/null;
15 | $F/busybox rm -rf $TMP 2>/dev/null;
16 | $F/busybox mkdir -p $TMP;
17 |
18 | $F/busybox mount -t tmpfs -o noatime tmpfs $TMP;
19 | #$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1;
20 |
21 | PATTERN='\$[Bb][Bb] chmod -R 755 tools bin;';
22 | sed -i "/$PATTERN/i cp -f \"\$F/mkbootfs\" \$AKHOME/tools;" "$F/update-binary";
23 |
24 | # update-binary
25 | AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z";
26 | RC=$?;
27 |
28 | $F/busybox umount $TMP;
29 | $F/busybox rm -rf $TMP;
30 | $F/busybox mount -o ro,remount -t auto /;
31 | $F/busybox rm -f $F/update-binary $F/busybox;
32 |
33 | # work around libsu not cleanly accepting return or exit as last line
34 | safereturn() { return $RC; }
35 | safereturn;
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material3.ButtonDefaults
7 | import androidx.compose.material3.Text
8 | import androidx.compose.material3.TextButton
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.res.stringResource
12 | import androidx.compose.ui.unit.LayoutDirection
13 | import androidx.compose.ui.unit.dp
14 | import com.github.capntrips.kernelflasher.R
15 |
16 | @Composable
17 | fun ViewButton(
18 | onClick: () -> Unit
19 | ) {
20 | TextButton(
21 | modifier = Modifier.padding(0.dp),
22 | shape = RoundedCornerShape(4.0.dp),
23 | contentPadding = PaddingValues(
24 | horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp,
25 | vertical = ButtonDefaults.ContentPadding.calculateTopPadding()
26 | ),
27 | onClick = onClick
28 | ) {
29 | Text(stringResource(R.string.view), maxLines = 1)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_splash_animation.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
17 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.shape.RoundedCornerShape
6 | import androidx.compose.material3.ButtonDefaults
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.material3.TextButton
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.LayoutDirection
13 | import androidx.compose.ui.unit.dp
14 |
15 | @Composable
16 | fun DialogButton(
17 | buttonText: String,
18 | onClick: () -> Unit
19 | ) {
20 | TextButton(
21 | modifier = Modifier.padding(0.dp),
22 | shape = RoundedCornerShape(4.0.dp),
23 | contentPadding = PaddingValues(
24 | horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp,
25 | vertical = ButtonDefaults.ContentPadding.calculateTopPadding()
26 | ),
27 | onClick = onClick
28 | ) {
29 | Text(
30 | buttonText,
31 | maxLines = 1,
32 | color = MaterialTheme.colorScheme.primary
33 | )
34 | }
35 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/MyOutlinedButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.shape.RoundedCornerShape
7 | import androidx.compose.material3.ButtonDefaults
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.OutlinedButton
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.unit.dp
13 |
14 | @Composable
15 | fun MyOutlinedButton(
16 | onclick: () -> Unit,
17 | enabled: Boolean = true,
18 | content: @Composable () -> Unit
19 | ) {
20 | OutlinedButton(
21 | modifier = Modifier
22 | .fillMaxWidth()
23 | .padding(horizontal = 4.dp, vertical = 0.dp),
24 | shape = RoundedCornerShape(10.dp),
25 | colors = ButtonDefaults.outlinedButtonColors(
26 | containerColor = MaterialTheme.colorScheme.secondaryContainer,
27 | contentColor = MaterialTheme.colorScheme.onSecondaryContainer
28 | ),
29 | border = BorderStroke(
30 | width = 1.2.dp,
31 | color = MaterialTheme.colorScheme.onSecondaryContainer.copy(alpha = 0.8f)
32 | ),
33 | enabled = enabled,
34 | onClick = onclick
35 | ) { content() }
36 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.reboot
2 |
3 | import android.os.PowerManager
4 | import androidx.compose.foundation.layout.ColumnScope
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.platform.LocalContext
8 | import androidx.compose.ui.res.stringResource
9 | import com.github.capntrips.kernelflasher.R
10 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
11 |
12 | @Suppress("UnusedReceiverParameter")
13 | @Composable
14 | fun ColumnScope.RebootContent(
15 | viewModel: RebootViewModel
16 | ) {
17 | val context = LocalContext.current
18 | MyOutlinedButton(
19 | { viewModel.rebootSystem() }
20 | ) {
21 | Text(stringResource(R.string.reboot))
22 | }
23 | if (context.getSystemService(PowerManager::class.java)?.isRebootingUserspaceSupported == true) {
24 | MyOutlinedButton(
25 | { viewModel.rebootUserspace() }
26 | ) {
27 | Text(stringResource(R.string.reboot_userspace))
28 | }
29 | }
30 | MyOutlinedButton(
31 | { viewModel.rebootRecovery() }
32 | ) {
33 | Text(stringResource(R.string.reboot_recovery))
34 | }
35 | MyOutlinedButton(
36 | { viewModel.rebootBootloader() }
37 | ) {
38 | Text(stringResource(R.string.reboot_bootloader))
39 | }
40 | MyOutlinedButton(
41 | { viewModel.rebootDownload() }
42 | ) {
43 | Text(stringResource(R.string.reboot_download))
44 | }
45 | MyOutlinedButton(
46 | { viewModel.rebootEdl() }
47 | ) {
48 | Text(stringResource(R.string.reboot_edl))
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.updates
2 |
3 | import androidx.compose.foundation.layout.ColumnScope
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.height
6 | import androidx.compose.material3.ExperimentalMaterial3Api
7 | import androidx.compose.material3.LocalTextStyle
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.text.font.FontFamily
12 | import androidx.compose.ui.unit.ExperimentalUnitApi
13 | import androidx.compose.ui.unit.TextUnit
14 | import androidx.compose.ui.unit.TextUnitType
15 | import androidx.compose.ui.unit.dp
16 | import androidx.navigation.NavController
17 | import com.github.capntrips.kernelflasher.ui.components.DataCard
18 | import kotlinx.serialization.ExperimentalSerializationApi
19 |
20 | @Suppress("UnusedReceiverParameter")
21 | @ExperimentalMaterial3Api
22 | @ExperimentalSerializationApi
23 | @ExperimentalUnitApi
24 | @Composable
25 | fun ColumnScope.UpdatesChangelogContent(
26 | viewModel: UpdatesViewModel,
27 | @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController
28 | ) {
29 | viewModel.currentUpdate?.let { currentUpdate ->
30 | DataCard(currentUpdate.kernelName)
31 | Spacer(Modifier.height(16.dp))
32 | Text(
33 | viewModel.changelog!!,
34 | style = LocalTextStyle.current.copy(
35 | fontFamily = FontFamily.Monospace,
36 | fontSize = TextUnit(12.0f, TextUnitType.Sp),
37 | lineHeight = TextUnit(18.0f, TextUnitType.Sp)
38 | )
39 | )
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_splash_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
13 |
17 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.partitions
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class Partitions(
7 | val boot: String? = null,
8 | val dtbo: String? = null,
9 | @Suppress("PropertyName") val init_boot: String? = null,
10 | val recovery: String? = null,
11 | @Suppress("PropertyName") val system_dlkm: String? = null,
12 | val vbmeta: String? = null,
13 | @Suppress("PropertyName") val vendor_boot: String? = null,
14 | @Suppress("PropertyName") val vendor_dlkm: String? = null,
15 | @Suppress("PropertyName") val vendor_kernel_boot: String? = null
16 | ) {
17 | companion object {
18 | fun from(sparseMap: Map) = object {
19 | val map = sparseMap.withDefault { null }
20 | val boot by map
21 | val dtbo by map
22 | val init_boot by map
23 | val recovery by map
24 | val system_dlkm by map
25 | val vbmeta by map
26 | val vendor_boot by map
27 | val vendor_dlkm by map
28 | val vendor_kernel_boot by map
29 | val partitions = Partitions(
30 | boot,
31 | dtbo,
32 | init_boot,
33 | recovery,
34 | system_dlkm,
35 | vbmeta,
36 | vendor_boot,
37 | vendor_dlkm,
38 | vendor_kernel_boot
39 | )
40 | }.partitions
41 | }
42 |
43 | fun get(partition: String): String? {
44 | return when (partition) {
45 | "boot" -> boot
46 | "dtbo" -> dtbo
47 | "init_boot" -> init_boot
48 | "recovery" -> recovery
49 | "system_dlkm" -> system_dlkm
50 | "vbmeta" -> vbmeta
51 | "vendor_boot" -> vendor_boot
52 | "vendor_dlkm" -> vendor_dlkm
53 | "vendor_kernel_boot" -> vendor_kernel_boot
54 | else -> null
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.error
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Warning
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.Icon
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Scaffold
16 | import androidx.compose.material3.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 | import com.github.capntrips.kernelflasher.ui.theme.Orange500
22 |
23 | @ExperimentalMaterial3Api
24 | @Composable
25 | fun ErrorScreen(message: String) {
26 | Scaffold { paddingValues ->
27 | Box(
28 | contentAlignment = Alignment.Center,
29 | modifier = Modifier
30 | .padding(paddingValues)
31 | .fillMaxSize()
32 | ) {
33 | Column(horizontalAlignment = Alignment.CenterHorizontally) {
34 | Icon(
35 | Icons.Filled.Warning,
36 | modifier = Modifier
37 | .width(48.dp)
38 | .height(48.dp),
39 | tint = Orange500,
40 | contentDescription = message
41 | )
42 | Spacer(Modifier.height(8.dp))
43 | Text(
44 | message,
45 | modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp),
46 | style = MaterialTheme.typography.titleLarge,
47 | )
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.updates
2 |
3 | import androidx.compose.foundation.layout.ColumnScope
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.material3.ExperimentalMaterial3Api
8 | import androidx.compose.material3.OutlinedTextField
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.unit.dp
19 | import androidx.navigation.NavController
20 | import com.github.capntrips.kernelflasher.R
21 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
22 | import kotlinx.serialization.ExperimentalSerializationApi
23 |
24 | @Suppress("UnusedReceiverParameter")
25 | @ExperimentalMaterial3Api
26 | @ExperimentalSerializationApi
27 | @Composable
28 | fun ColumnScope.UpdatesAddContent(
29 | viewModel: UpdatesViewModel,
30 | navController: NavController
31 | ) {
32 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current
33 | var url by remember { mutableStateOf("") }
34 | OutlinedTextField(
35 | value = url,
36 | onValueChange = { url = it },
37 | label = { Text(stringResource(R.string.url)) },
38 | modifier = Modifier
39 | .fillMaxWidth()
40 | )
41 | Spacer(Modifier.height(5.dp))
42 | MyOutlinedButton(
43 | { viewModel.add(url) { navController.navigate("updates/view/$it") { popUpTo("updates") } } }
44 | ) {
45 | Text(stringResource(R.string.add))
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataValue.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.RowScope
6 | import androidx.compose.foundation.text.selection.SelectionContainer
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.text.TextStyle
17 | import androidx.compose.ui.text.style.TextOverflow
18 | import androidx.compose.ui.tooling.preview.Preview
19 | import androidx.compose.ui.unit.sp
20 |
21 | @Composable
22 | fun RowScope.DataValue(
23 | value: String,
24 | color: Color = Color.Unspecified,
25 | style: TextStyle = MaterialTheme.typography.titleSmall,
26 | clickable: Boolean = false,
27 | ) {
28 | SelectionContainer(
29 | modifier = Modifier.alignByBaseline()
30 | ) {
31 | var clicked by remember { mutableStateOf(false) }
32 | val textModifier = if (clickable) {
33 | Modifier
34 | .clickable { clicked = !clicked }
35 | .alignByBaseline()
36 | } else {
37 | Modifier.alignByBaseline()
38 | }
39 | Text(
40 | modifier = textModifier,
41 | fontSize = 13.5.sp,
42 | text = value,
43 | color = color,
44 | style = style,
45 | maxLines = if (clicked) Int.MAX_VALUE else 1,
46 | overflow = if (clicked) TextOverflow.Visible else TextOverflow.Ellipsis
47 | )
48 | }
49 | }
50 |
51 | @Preview
52 | @Composable
53 | fun DataValuePreview() {
54 | Row {
55 | DataValue(
56 | value = "Example Value",
57 | color = Color.Black,
58 | style = MaterialTheme.typography.headlineSmall,
59 | clickable = true
60 | )
61 | }
62 | }
63 |
64 |
65 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Android Build
2 | permissions:
3 | contents: write
4 | on:
5 | workflow_dispatch:
6 | push:
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout repository
14 | uses: actions/checkout@v4
15 |
16 | - name: Set up JDK 21
17 | uses: actions/setup-java@v4
18 | with:
19 | distribution: "temurin"
20 | java-version: 21
21 |
22 | - name: Setup Gradle
23 | uses: gradle/actions/setup-gradle@v4
24 |
25 | - name: Build with Gradle
26 | run: |
27 | chmod +x ./gradlew
28 | ./gradlew assembleRelease
29 | tree app/build/outputs/apk/release
30 |
31 | - uses: r0adkll/sign-android-release@v1.0.4
32 | name: Sign app APK
33 | id: sign_app
34 | with:
35 | releaseDirectory: app/build/outputs/apk/release
36 | signingKeyBase64: ${{ secrets.KEYSTORE }}
37 | alias: ${{ secrets.KEY_ALIAS }}
38 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
39 | keyPassword: ${{ secrets.KEY_PASSWORD }}
40 | env:
41 | BUILD_TOOLS_VERSION: "36.0.0"
42 |
43 | - name: Rename APK
44 | run: |
45 | ls -al app/build/outputs/apk/release
46 | echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}"
47 | cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher.apk
48 |
49 | - name: Upload APK
50 | uses: actions/upload-artifact@v4.3.5
51 | with:
52 | name: KernelFlasher
53 | path: KernelFlasher.apk
54 |
55 | - name: Rename apk
56 | run: |
57 | ls -al
58 | DATE=$(date +'%y.%m.%d')
59 | echo "TAG=$DATE" >> $GITHUB_ENV
60 |
61 | # - name: Upload release
62 | # uses: ncipollo/release-action@v1.14.0
63 | # with:
64 | # allowUpdates: true
65 | # removeArtifacts: true
66 | # name: "1.${{ github.run_number }}.0"
67 | # tag: "v1.${{ github.run_number }}.0"
68 | # body: |
69 | # Note: QMod KernelFlasher, support ksu-lkm
70 | # artifacts: "*.apk"
71 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Android Release
2 | permissions:
3 | contents: write
4 | on:
5 | workflow_dispatch:
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Checkout repository
13 | uses: actions/checkout@v4
14 |
15 | - name: Set up JDK 21
16 | uses: actions/setup-java@v4
17 | with:
18 | distribution: "temurin"
19 | java-version: 21
20 |
21 | - name: Setup Gradle
22 | uses: gradle/actions/setup-gradle@v4
23 |
24 | - name: Build with Gradle
25 | run: |
26 | chmod +x ./gradlew
27 | ./gradlew assembleRelease
28 | tree app/build/outputs/apk/release
29 |
30 | - uses: qlenlen/sign-android-release@v2.0.1
31 | name: Sign app APK
32 | id: sign_app
33 | with:
34 | releaseDirectory: app/build/outputs/apk/release
35 | signingKeyBase64: ${{ secrets.KEYSTORE }}
36 | alias: ${{ secrets.KEY_ALIAS }}
37 | keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
38 | keyPassword: ${{ secrets.KEY_PASSWORD }}
39 | env:
40 | BUILD_TOOLS_VERSION: "36.0.0"
41 |
42 | - name: Rename APK
43 | run: |
44 | ls -al app/build/outputs/apk/release
45 | echo "Signed APK: ${{steps.sign_app.outputs.signedReleaseFile}}"
46 | cp ${{steps.sign_app.outputs.signedReleaseFile}} KernelFlasher.apk
47 |
48 | - name: Upload APK
49 | uses: actions/upload-artifact@v4.3.5
50 | with:
51 | name: KernelFlasher
52 | path: KernelFlasher.apk
53 |
54 | - name: Rename apk
55 | run: |
56 | ls -al
57 | DATE=$(date +'%y.%m.%d')
58 | echo "TAG=$DATE" >> $GITHUB_ENV
59 |
60 | - name: Upload release
61 | uses: ncipollo/release-action@v1.14.0
62 | with:
63 | allowUpdates: true
64 | removeArtifacts: true
65 | name: "1.${{ github.run_number }}.0"
66 | tag: "v1.${{ github.run_number }}.0"
67 | body: |
68 | Note: QMod KernelFlasher, support ksu-lkm
69 | artifacts: "*.apk"
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Text
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.MutableState
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.layout.layout
16 | import androidx.compose.ui.text.TextStyle
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.unit.sp
19 |
20 | @Composable
21 | fun DataRow(
22 | label: String,
23 | value: String,
24 | labelColor: Color = Color.Unspecified,
25 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium,
26 | valueColor: Color = Color.Unspecified,
27 | valueStyle: TextStyle = MaterialTheme.typography.titleSmall,
28 | mutableMaxWidth: MutableState? = null,
29 | clickable: Boolean = false,
30 | ) {
31 | Row {
32 | val labelModifier = if (mutableMaxWidth != null) {
33 | var maxWidth by mutableMaxWidth
34 | Modifier
35 | .padding(bottom = 6.dp)
36 | .layout { measurable, constraints ->
37 | val placeable = measurable.measure(constraints)
38 | maxWidth = maxOf(maxWidth, placeable.width)
39 | layout(width = maxWidth, height = placeable.height) {
40 | placeable.placeRelative(0, 0)
41 | }
42 | }
43 | .alignByBaseline()
44 | } else {
45 | Modifier.alignByBaseline()
46 | }
47 | Text(
48 | modifier = labelModifier.then(Modifier.padding(top = 2.dp)),
49 | text = label,
50 | color = labelColor,
51 | style = labelStyle,
52 | fontSize = 12.5.sp
53 | )
54 | Spacer(Modifier.width(12.dp))
55 | DataValue(
56 | value = value,
57 | color = valueColor,
58 | style = valueStyle,
59 | clickable = clickable,
60 | )
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material3.Card
13 | import androidx.compose.material3.CardDefaults
14 | import androidx.compose.material3.MaterialTheme
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.text.font.FontWeight
20 | import androidx.compose.ui.unit.dp
21 |
22 | @Composable
23 | fun DataCard(
24 | title: String,
25 | button: @Composable (() -> Unit)? = null,
26 | content: @Composable (ColumnScope.() -> Unit)? = null
27 | ) {
28 | Card(
29 | modifier = Modifier
30 | .fillMaxWidth(),
31 | shape = RoundedCornerShape(12.dp),
32 | elevation = CardDefaults.cardElevation(
33 | defaultElevation = 8.dp
34 | ),
35 | colors = CardDefaults.cardColors(
36 | containerColor = MaterialTheme.colorScheme.primaryContainer
37 | )
38 | ) {
39 | Column(
40 | modifier = Modifier.padding(16.dp)
41 | ) {
42 | Row(
43 | modifier = Modifier.fillMaxWidth(),
44 | horizontalArrangement = Arrangement.SpaceBetween,
45 | verticalAlignment = Alignment.CenterVertically
46 | ) {
47 | Text(
48 | text = title,
49 | color = MaterialTheme.colorScheme.onPrimaryContainer,
50 | style = MaterialTheme.typography.titleLarge,
51 | fontWeight = FontWeight.ExtraBold
52 | )
53 | if (button != null) {
54 | button()
55 | }
56 | }
57 |
58 | if (content != null) {
59 | Spacer(Modifier.height(14.dp))
60 | content()
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.reboot
2 |
3 | import android.content.Context
4 | import android.util.Log
5 | import androidx.compose.runtime.MutableState
6 | import androidx.lifecycle.ViewModel
7 | import androidx.lifecycle.viewModelScope
8 | import androidx.navigation.NavController
9 | import com.topjohnwu.superuser.Shell
10 | import com.topjohnwu.superuser.nio.FileSystemManager
11 | import kotlinx.coroutines.Dispatchers
12 | import kotlinx.coroutines.launch
13 | import kotlinx.coroutines.withContext
14 |
15 | class RebootViewModel(
16 | @Suppress("UNUSED_PARAMETER") ignoredContext: Context,
17 | @Suppress("unused") private val fileSystemManager: FileSystemManager,
18 | private val navController: NavController,
19 | private val _isRefreshing: MutableState
20 | ) : ViewModel() {
21 | companion object {
22 | const val TAG: String = "KernelFlasher/RebootState"
23 | }
24 |
25 | val isRefreshing: Boolean
26 | get() = _isRefreshing.value
27 |
28 | private fun launch(block: suspend () -> Unit) {
29 | viewModelScope.launch(Dispatchers.IO) {
30 | _isRefreshing.value = true
31 | try {
32 | block()
33 | } catch (e: Exception) {
34 | withContext(Dispatchers.Main) {
35 | Log.e(TAG, e.message, e)
36 | navController.navigate("error/${e.message}") {
37 | popUpTo("main")
38 | }
39 | }
40 | }
41 | _isRefreshing.value = false
42 | }
43 | }
44 |
45 | private fun reboot(destination: String = "") {
46 | launch {
47 | // https://github.com/topjohnwu/Magisk/blob/v25.2/app/src/main/java/com/topjohnwu/magisk/ktx/XSU.kt#L11-L15
48 | if (destination == "recovery") {
49 | // https://github.com/topjohnwu/Magisk/pull/5637
50 | Shell.cmd("/system/bin/input keyevent 26").submit()
51 | }
52 | Shell.cmd("/system/bin/svc power reboot $destination || /system/bin/reboot $destination")
53 | .submit()
54 | }
55 | }
56 |
57 | fun rebootSystem() {
58 | reboot()
59 | }
60 |
61 | fun rebootUserspace() {
62 | reboot("userspace")
63 | }
64 |
65 | fun rebootRecovery() {
66 | reboot("recovery")
67 | }
68 |
69 | fun rebootBootloader() {
70 | reboot("bootloader")
71 | }
72 |
73 | fun rebootDownload() {
74 | reboot("download")
75 | }
76 |
77 | fun rebootEdl() {
78 | reboot("edl")
79 | }
80 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import android.net.Uri
4 | import android.provider.OpenableColumns
5 | import android.widget.Toast
6 | import androidx.activity.compose.LocalActivity
7 | import androidx.activity.compose.rememberLauncherForActivityResult
8 | import androidx.activity.result.contract.ActivityResultContracts
9 | import androidx.compose.animation.ExperimentalAnimationApi
10 | import androidx.compose.material.ExperimentalMaterialApi
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.unit.ExperimentalUnitApi
17 | import com.github.capntrips.kernelflasher.MainActivity
18 | import kotlinx.serialization.ExperimentalSerializationApi
19 |
20 | @ExperimentalAnimationApi
21 | @ExperimentalMaterialApi
22 | @ExperimentalMaterial3Api
23 | @ExperimentalSerializationApi
24 | @ExperimentalUnitApi
25 | @Composable
26 | fun FlashButton(
27 | buttonText: String,
28 | validExtension: String,
29 | callback: (uri: Uri) -> Unit
30 | ) {
31 | val mainActivity = LocalActivity.current as MainActivity
32 | val result = remember { mutableStateOf(null) }
33 | val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
34 | result.value = it
35 | if (it == null) {
36 | mainActivity.isAwaitingResult = false
37 | }
38 | }
39 | MyOutlinedButton(
40 | {
41 | mainActivity.isAwaitingResult = true
42 | launcher.launch("*/*")
43 | }
44 | ) {
45 | Text(buttonText)
46 | }
47 | result.value?.let { uri ->
48 | if (mainActivity.isAwaitingResult) {
49 | val contentResolver = mainActivity.contentResolver
50 | val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
51 | val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
52 | if (nameIndex != -1 && cursor.moveToFirst()) {
53 | cursor.getString(nameIndex)
54 | } else {
55 | null
56 | }
57 | }
58 |
59 | if (fileName != null && fileName.endsWith(validExtension, ignoreCase = true)) {
60 | callback.invoke(uri)
61 | } else {
62 | // Invalid file extension, show an error message or handle it
63 | Toast.makeText(mainActivity.applicationContext, "Invalid file selected!", Toast.LENGTH_LONG)
64 | .show()
65 | }
66 | }
67 | mainActivity.isAwaitingResult = false
68 | }
69 | mainActivity.isAwaitingResult = false
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 capntrips
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
21 |
22 | This project bundles lptools (https://github.com/phhusson/vendor_lptools),
23 | which is licensed under the Apache 2.0 license:
24 |
25 | Copyright (C) 2020 Pierre-Hugues Husson
26 |
27 | Licensed under the Apache License, Version 2.0 (the "License");
28 | you may not use this file except in compliance with the License.
29 | You may obtain a copy of the License at
30 |
31 | http://www.apache.org/licenses/LICENSE-2.0
32 |
33 | Unless required by applicable law or agreed to in writing, software
34 | distributed under the License is distributed on an "AS IS" BASIS,
35 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
36 | See the License for the specific language governing permissions and
37 | limitations under the License.
38 |
39 | This project bundles magiskboot (https://github.com/topjohnwu/Magisk),
40 | which is licensed under the GPLv3+ license:
41 |
42 | Copyright (C) 2017-2022 John Wu <@topjohnwu>
43 |
44 | This program is free software: you can redistribute it and/or modify
45 | it under the terms of the GNU General Public License as published by
46 | the Free Software Foundation, either version 3 of the License, or
47 | (at your option) any later version.
48 |
49 | This program is distributed in the hope that it will be useful,
50 | but WITHOUT ANY WARRANTY; without even the implied warranty of
51 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
52 | GNU General Public License for more details.
53 |
54 | You should have received a copy of the GNU General Public License
55 | along with this program. If not, see .
56 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rTW/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kernel Flasher
4 | 需要 Root 授權
5 | Root 服務已斷開
6 | 裝置
7 | 型號
8 | 構建版本
9 | 核心名
10 | 核心版本
11 | 插槽字尾
12 | 插槽
13 | 插槽 A
14 | 插槽 B
15 | Boot 雜湊
16 | Vendor DLKM
17 | 存在
18 | 未找到
19 | 已掛載
20 | 未解除安裝
21 | 檢視
22 | 備份
23 | 儲存 ramoops
24 | 儲存 dmesg
25 | 儲存 logcat
26 | 返回
27 | 備份
28 | 更新
29 | 刷入
30 | 刷入 AK3 壓縮包
31 | 刷入分割槽映象
32 | " KernelSU LKM Driver"
33 | 還原
34 | 檢查核心版本
35 | 掛載 Vendor DLKM
36 | 解除安裝 Vendor DLKM
37 | 對映 Vendor DLKM
38 | 取消對映 Vendor DLKM
39 | 遷移
40 | 沒有找到備份
41 | 刪除
42 | 新增
43 | 連結地址
44 | 版本
45 | 釋出日期
46 | 更新日期
47 | 變更日誌
48 | 檢查更新
49 | 下載
50 | 重啟
51 | 軟重啟
52 | 重啟到 Recovery
53 | 重啟到 Bootloader
54 | 重啟到 Download
55 | 重啟到 EDL
56 | 儲存 AK3 日誌
57 | 儲存刷寫日誌
58 | 儲存備份日誌
59 | 儲存還原日誌
60 | 將 AK3 包作為備份儲存
61 | 備份型別
62 | 雜湊值
63 | 舊的備份無法選擇分割槽
64 | 不支持
65 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.devtools.ksp)
4 | alias(libs.plugins.kotlin.android)
5 | alias(libs.plugins.kotlin.serialization)
6 | alias(libs.plugins.compose.compiler)
7 | }
8 |
9 | android {
10 | compileSdk 36
11 |
12 | defaultConfig {
13 | applicationId "com.github.capntrips.kernelflasher"
14 | minSdk 33
15 | targetSdk 36
16 | versionCode 30
17 | versionName "1.0.0-alpha30"
18 |
19 | javaCompileOptions {
20 | annotationProcessorOptions {
21 | arguments += [
22 | "room.schemaLocation": "$projectDir/schemas".toString(),
23 | "room.incremental" : "true"
24 | ]
25 | }
26 | }
27 | ndk {
28 | //noinspection ChromeOsAbiSupport
29 | abiFilters = ['arm64-v8a']
30 | }
31 | vectorDrawables {
32 | useSupportLibrary true
33 | }
34 | }
35 | buildTypes {
36 | release {
37 | debuggable false
38 | minifyEnabled true
39 | shrinkResources true
40 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
41 | }
42 | }
43 | sourceSets {
44 | main {
45 | jniLibs.srcDirs = ['src/main/jniLibs']
46 | }
47 | }
48 | buildFeatures {
49 | aidl true
50 | }
51 | compileOptions {
52 | sourceCompatibility JavaVersion.VERSION_17
53 | targetCompatibility JavaVersion.VERSION_17
54 | }
55 | kotlinOptions {
56 | jvmTarget = '17'
57 | }
58 | buildFeatures {
59 | compose true
60 | }
61 | packagingOptions {
62 | resources {
63 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
64 | }
65 | jniLibs {
66 | useLegacyPackaging true
67 | }
68 | }
69 | namespace 'com.github.capntrips.kernelflasher'
70 | }
71 |
72 | dependencies {
73 | implementation(libs.androidx.activity.compose)
74 | implementation(libs.androidx.material.icons.extended)
75 | implementation(libs.androidx.appcompat)
76 | implementation(libs.androidx.compose.material)
77 | implementation(libs.androidx.compose.material3)
78 | implementation(libs.androidx.compose.foundation)
79 | implementation(libs.androidx.compose.ui)
80 | implementation(libs.androidx.core.ktx)
81 | implementation(libs.androidx.core.splashscreen)
82 | implementation(libs.androidx.lifecycle.runtime.ktx)
83 | implementation(libs.androidx.lifecycle.viewmodel.compose)
84 | implementation(libs.androidx.navigation.compose)
85 | implementation(libs.androidx.room.runtime)
86 | implementation libs.androidx.ui.tooling.preview.android
87 | implementation libs.androidx.animation.core.android
88 | annotationProcessor(libs.androidx.room.compiler)
89 | ksp(libs.androidx.room.compiler)
90 | implementation(libs.libsu.core)
91 | implementation(libs.libsu.io)
92 | implementation(libs.libsu.nio)
93 | implementation(libs.libsu.service)
94 | implementation(libs.material)
95 | implementation(libs.okhttp)
96 | implementation(libs.kotlinx.serialization.json)
97 | }
98 |
--------------------------------------------------------------------------------
/app/src/main/res/values-zh-rCN/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kernel Flasher
4 | 需要 Root 权限
5 | Root 服务已断开
6 | 设备
7 | 型号
8 | 构建版本
9 | 内核名
10 | 内核版本
11 | 插槽后缀
12 | 插槽 A
13 | 插槽 B
14 | Boot 哈希
15 | Vendor DLKM
16 | 存在
17 | 未找到
18 | 已挂载
19 | 未卸载
20 | 查看
21 | 备份
22 | 保存 ramoops
23 | 保存 dmesg
24 | 保存 logcat
25 | 返回
26 | 备份
27 | 更新
28 | 刷入
29 | 使用 mkbootfs 刷入
30 | 刷入 AK3 压缩包
31 | 刷入 AK3 压缩包 使用 mkbootfs
32 | 刷入分区镜像
33 | 刷入 KernelSU LKM Driver
34 | 恢复
35 | 检查内核版本
36 | 挂载 Vendor DLKM
37 | 卸载 Vendor DLKM
38 | 映射 Vendor DLKM
39 | 取消映射 Vendor DLKM
40 | 迁移
41 | 没有找到备份
42 | 删除
43 | 添加
44 | 链接地址
45 | 版本
46 | 发布日期
47 | 更新日期
48 | 变更日志
49 | 检查更新
50 | 下载
51 | 重启
52 | 软重启
53 | 重启到 Recovery
54 | 重启到 Bootloader
55 | 重启到 Download
56 | 重启到 EDL
57 | 保存 AK3 日志
58 | 保存刷写日志
59 | 保存备份日志
60 | 保存恢复日志
61 | 将 AK3 包作为备份保存
62 | 备份类型
63 | 哈希值
64 | 旧的备份无法选择分区
65 | boot.img 格式
66 | init_boot.img 格式
67 | Ramdisk 格式
68 | SUSFS 版本
69 | 不支持
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.updates
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.mutableIntStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.text.font.FontStyle
18 | import androidx.compose.ui.unit.dp
19 | import androidx.navigation.NavController
20 | import com.github.capntrips.kernelflasher.R
21 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer
22 | import com.github.capntrips.kernelflasher.ui.components.DataCard
23 | import com.github.capntrips.kernelflasher.ui.components.DataRow
24 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
25 | import com.github.capntrips.kernelflasher.ui.components.ViewButton
26 | import kotlinx.serialization.ExperimentalSerializationApi
27 |
28 | @ExperimentalMaterial3Api
29 | @ExperimentalSerializationApi
30 | @Composable
31 | fun ColumnScope.UpdatesContent(
32 | viewModel: UpdatesViewModel,
33 | navController: NavController
34 | ) {
35 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current
36 | DataCard(stringResource(R.string.updates))
37 | if (viewModel.updates.isNotEmpty()) {
38 | for (update in viewModel.updates.sortedByDescending { it.kernelDate }) {
39 | Spacer(Modifier.height(16.dp))
40 | DataCard(
41 | title = update.kernelName,
42 | button = {
43 | AnimatedVisibility(!viewModel.isRefreshing) {
44 | Column {
45 | ViewButton(onClick = {
46 | navController.navigate("updates/view/${update.id}")
47 | })
48 | }
49 | }
50 | }
51 | ) {
52 | val cardWidth = remember { mutableIntStateOf(0) }
53 | DataRow(stringResource(R.string.version), update.kernelVersion, mutableMaxWidth = cardWidth)
54 | DataRow(
55 | stringResource(R.string.date_released),
56 | DateSerializer.formatter.format(update.kernelDate),
57 | mutableMaxWidth = cardWidth
58 | )
59 | DataRow(
60 | label = stringResource(R.string.last_updated),
61 | value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!),
62 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
63 | labelStyle = MaterialTheme.typography.labelMedium.copy(
64 | fontStyle = FontStyle.Italic
65 | ),
66 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
67 | valueStyle = MaterialTheme.typography.titleSmall.copy(
68 | fontStyle = FontStyle.Italic
69 | ),
70 | mutableMaxWidth = cardWidth
71 | )
72 | }
73 | }
74 | }
75 | AnimatedVisibility(!viewModel.isRefreshing) {
76 | Column {
77 | Spacer(Modifier.height(12.dp))
78 | MyOutlinedButton(
79 | { navController.navigate("updates/add") }
80 | ) {
81 | Text(stringResource(R.string.add))
82 | }
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.updates
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.mutableIntStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.res.stringResource
17 | import androidx.compose.ui.text.font.FontStyle
18 | import androidx.compose.ui.unit.dp
19 | import androidx.navigation.NavController
20 | import com.github.capntrips.kernelflasher.R
21 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer
22 | import com.github.capntrips.kernelflasher.ui.components.DataCard
23 | import com.github.capntrips.kernelflasher.ui.components.DataRow
24 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
25 | import kotlinx.serialization.ExperimentalSerializationApi
26 |
27 | @ExperimentalMaterial3Api
28 | @ExperimentalSerializationApi
29 | @Composable
30 | fun ColumnScope.UpdatesViewContent(
31 | viewModel: UpdatesViewModel,
32 | navController: NavController
33 | ) {
34 | val context = LocalContext.current
35 | viewModel.currentUpdate?.let { currentUpdate ->
36 | DataCard(currentUpdate.kernelName) {
37 | val cardWidth = remember { mutableIntStateOf(0) }
38 | DataRow(
39 | stringResource(R.string.version),
40 | currentUpdate.kernelVersion,
41 | mutableMaxWidth = cardWidth
42 | )
43 | DataRow(
44 | stringResource(R.string.date_released),
45 | DateSerializer.formatter.format(currentUpdate.kernelDate),
46 | mutableMaxWidth = cardWidth
47 | )
48 | DataRow(
49 | label = stringResource(R.string.last_updated),
50 | value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!),
51 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
52 | labelStyle = MaterialTheme.typography.labelMedium.copy(
53 | fontStyle = FontStyle.Italic
54 | ),
55 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
56 | valueStyle = MaterialTheme.typography.titleSmall.copy(
57 | fontStyle = FontStyle.Italic,
58 | ),
59 | mutableMaxWidth = cardWidth
60 | )
61 | }
62 | AnimatedVisibility(!viewModel.isRefreshing) {
63 | Column {
64 | Spacer(Modifier.height(5.dp))
65 | MyOutlinedButton(
66 | { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } }
67 | ) {
68 | Text(stringResource(R.string.changelog))
69 | }
70 | // TODO: add download progress indicator
71 | MyOutlinedButton(
72 | { viewModel.downloadKernel(context) }
73 | ) {
74 | Text(stringResource(R.string.download))
75 | }
76 | MyOutlinedButton(
77 | { viewModel.update() }
78 | ) {
79 | Text(stringResource(R.string.check_for_updates))
80 | }
81 | MyOutlinedButton(
82 | { viewModel.delete { navController.popBackStack() } }
83 | ) {
84 | Text(stringResource(R.string.delete))
85 | }
86 | }
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.material3.ExperimentalMaterial3Api
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.mutableIntStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.res.stringResource
11 | import androidx.compose.ui.text.font.FontFamily
12 | import androidx.compose.ui.text.font.FontWeight
13 | import androidx.navigation.NavController
14 | import com.github.capntrips.kernelflasher.R
15 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel
16 |
17 | @ExperimentalMaterial3Api
18 | @Composable
19 | fun SlotCard(
20 | title: String,
21 | viewModel: SlotViewModel,
22 | navController: NavController,
23 | isSlotScreen: Boolean = false,
24 | showDlkm: Boolean = true,
25 | ) {
26 | DataCard(
27 | title = title,
28 | button = {
29 | if (!isSlotScreen) {
30 | AnimatedVisibility(!viewModel.isRefreshing.value) {
31 | ViewButton {
32 | navController.navigate("slot${viewModel.slotSuffix}")
33 | }
34 | }
35 | }
36 | }
37 | ) {
38 | val cardWidth = remember { mutableIntStateOf(0) }
39 | if (!viewModel.sha1.isNullOrEmpty()) {
40 | DataRow(
41 | label = stringResource(R.string.boot_sha1),
42 | value = viewModel.sha1!!.substring(0, 8),
43 | valueStyle = MaterialTheme.typography.titleSmall.copy(
44 | fontFamily = FontFamily.Monospace,
45 | fontWeight = FontWeight.Medium
46 | ),
47 | mutableMaxWidth = cardWidth
48 | )
49 | }
50 | AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.bootInfo.kernelVersion != null) {
51 | DataRow(
52 | label = stringResource(R.string.kernel_version),
53 | value = if (viewModel.bootInfo.kernelVersion != null) viewModel.bootInfo.kernelVersion!! else "",
54 | mutableMaxWidth = cardWidth,
55 | clickable = true
56 | )
57 | }
58 | if (showDlkm && viewModel.hasVendorDlkm) {
59 | var vendorDlkmValue = stringResource(R.string.not_found)
60 | if (viewModel.isVendorDlkmMapped) {
61 | vendorDlkmValue = if (viewModel.isVendorDlkmMounted) {
62 | String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.mounted))
63 | } else {
64 | String.format(
65 | "%s, %s",
66 | stringResource(R.string.exists),
67 | stringResource(R.string.unmounted)
68 | )
69 | }
70 | }
71 | DataRow(stringResource(R.string.vendor_dlkm), vendorDlkmValue, mutableMaxWidth = cardWidth)
72 | }
73 | DataRow(
74 | label = stringResource(R.string.boot_fmt),
75 | value = viewModel.bootInfo.bootFmt ?: stringResource(R.string.not_found),
76 | mutableMaxWidth = cardWidth
77 | )
78 | DataRow(
79 | label = if (viewModel.bootInfo.ramdiskLocation == "init_boot.img") stringResource(R.string.init_boot_fmt) else stringResource(
80 | R.string.ramdisk_fmt
81 | ),
82 | value = viewModel.bootInfo.initBootFmt ?: stringResource(R.string.not_found),
83 | mutableMaxWidth = cardWidth
84 | )
85 | if (!viewModel.isRefreshing.value && viewModel.hasError) {
86 | Row {
87 | DataValue(
88 | value = viewModel.error ?: "",
89 | color = MaterialTheme.colorScheme.error,
90 | style = MaterialTheme.typography.titleSmall,
91 | clickable = true
92 | )
93 | }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common.types.room.updates
2 |
3 | import androidx.room.ColumnInfo
4 | import androidx.room.Entity
5 | import androidx.room.PrimaryKey
6 | import kotlinx.serialization.KSerializer
7 | import kotlinx.serialization.Serializable
8 | import kotlinx.serialization.Transient
9 | import kotlinx.serialization.descriptors.PrimitiveKind
10 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
11 | import kotlinx.serialization.encoding.Decoder
12 | import kotlinx.serialization.encoding.Encoder
13 | import kotlinx.serialization.json.JsonElement
14 | import kotlinx.serialization.json.JsonObject
15 | import kotlinx.serialization.json.JsonTransformingSerializer
16 | import kotlinx.serialization.json.buildJsonObject
17 | import java.text.SimpleDateFormat
18 | import java.util.Date
19 | import java.util.Locale
20 |
21 | object DateSerializer : KSerializer {
22 | override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING)
23 | val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US)
24 | override fun serialize(encoder: Encoder, value: Date) =
25 | encoder.encodeString(formatter.format(value))
26 |
27 | override fun deserialize(decoder: Decoder): Date = formatter.parse(decoder.decodeString())!!
28 | }
29 |
30 | object UpdateSerializer : JsonTransformingSerializer(Update.serializer()) {
31 | override fun transformSerialize(element: JsonElement): JsonElement {
32 | require(element is JsonObject)
33 | return buildJsonObject {
34 | put("kernel", buildJsonObject {
35 | put("name", element["kernelName"]!!)
36 | put("version", element["kernelVersion"]!!)
37 | put("link", element["kernelLink"]!!)
38 | put("changelog_url", element["kernelChangelogUrl"]!!)
39 | put("date", element["kernelDate"]!!)
40 | put("sha1", element["kernelSha1"]!!)
41 | })
42 | if (element["supportLink"] != null) {
43 | put("support", buildJsonObject {
44 | put("link", element["supportLink"]!!)
45 | })
46 | }
47 | }
48 | }
49 |
50 | override fun transformDeserialize(element: JsonElement): JsonElement {
51 | require(element is JsonObject)
52 | val kernel = element["kernel"]
53 | val support = element["support"]
54 | require(kernel is JsonObject)
55 | require(support is JsonObject?)
56 | return buildJsonObject {
57 | put("kernelName", kernel["name"]!!)
58 | put("kernelVersion", kernel["version"]!!)
59 | put("kernelLink", kernel["link"]!!)
60 | put("kernelChangelogUrl", kernel["changelog_url"]!!)
61 | put("kernelDate", kernel["date"]!!)
62 | put("kernelSha1", kernel["sha1"]!!)
63 | if (support != null && support["link"] != null) {
64 | put("supportLink", support["link"]!!)
65 | }
66 | }
67 | }
68 | }
69 |
70 | @Entity
71 | @Serializable
72 | data class Update(
73 | @PrimaryKey
74 | @Transient
75 | val id: Int? = null,
76 | @ColumnInfo(name = "update_uri")
77 | @Transient
78 | var updateUri: String? = null,
79 | @ColumnInfo(name = "kernel_name")
80 | var kernelName: String,
81 | @ColumnInfo(name = "kernel_version")
82 | var kernelVersion: String,
83 | @ColumnInfo(name = "kernel_link")
84 | var kernelLink: String,
85 | @ColumnInfo(name = "kernel_changelog_url")
86 | var kernelChangelogUrl: String,
87 | @ColumnInfo(name = "kernel_date")
88 | @Serializable(DateSerializer::class)
89 | var kernelDate: Date,
90 | @ColumnInfo(name = "kernel_sha1")
91 | var kernelSha1: String,
92 | @ColumnInfo(name = "support_link")
93 | var supportLink: String?,
94 | @ColumnInfo(name = "last_updated")
95 | @Transient
96 | var lastUpdated: Date? = null,
97 | )
98 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ja/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kernel Flasher
4 | Root 権限が必要です
5 | Root サービスが切断されました
6 | デバイス
7 | モデル
8 | ビルド番号
9 | カーネル名
10 | カーネルバージョン
11 | スロットの接頭辞
12 | スロット
13 | スロット A
14 | スロット B
15 | Boot SHA1
16 | Vendor DLKM
17 | あり
18 | なし
19 | マウント済み
20 | アンマウント済み
21 | 表示
22 | バックアップ
23 | ramoops を保存
24 | dmesg を保存
25 | logcat を保存
26 | 戻る
27 | バックアップ
28 | 更新
29 | フラッシュ
30 | mkbootfsを使用してフラッシュする
31 | AK3 Zip をフラッシュ
32 | mkbootfs を使って AK3 Zip をフラッシュする
33 | パーティションイメージをフラッシュ
34 | " KernelSU LKM Driver"
35 | 復元
36 | カーネルバージョンを確認
37 | Vendor DLKM をマウント
38 | Vendor DLKM をアンマウント
39 | Vendor DLKM をマップ
40 | Vendor DLKM をアンマップ
41 | 移行
42 | バックアップが見つかりません
43 | 削除
44 | 追加
45 | URL
46 | バージョン
47 | リリース日
48 | 最終更新
49 | 更新履歴
50 | 更新を確認
51 | ダウンロード
52 | 再起動
53 | ソフトリブート
54 | リカバリーで再起動
55 | ブートローダーで再起動
56 | ダウンロードモードで再起動
57 | EDL で再起動
58 | AK3 ログを保存
59 | フラッシュログを保存
60 | バックアップログを保存
61 | 復元ログを保存
62 | AK3 Zip をバックアップとして保存
63 | バックアップタイプ
64 | ハッシュ
65 | レガシーバックアップではパーティションを選択できません
66 | boot.img形式
67 | init_boot.img形式
68 | Ramdisk形式
69 | SUSFSバージョン
70 | unsupported
71 |
72 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.3.0"
3 |
4 | androidx-activity-compose = "1.12.2"
5 | androidx-appcompat = "1.7.1"
6 | androidx-compose = "1.10.0"
7 | androidx-compose-material3 = "1.4.0"
8 | androidx-core-ktx = "1.17.0"
9 | androidx-core-splashscreen = "1.2.0"
10 | androidx-lifecycle = "2.10.0"
11 | androidx-navigation-compose = "2.9.6"
12 | androidx-room = "2.8.4"
13 | kotlinx-serialization-json = "1.9.0"
14 | libsu = "5.2.1"
15 | material = "1.13.0"
16 | okhttp = "5.3.2"
17 |
18 | android-application = "8.13.2"
19 | devtools-ksp = "2.3.4"
20 | ui-tooling-preview-android = "1.10.0"
21 | animation-core-android = "1.10.0"
22 |
23 |
24 | [libraries]
25 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
26 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
27 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" }
28 | androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose" }
29 | androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "androidx-compose-material3" }
30 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
31 | androidx-compose-ui = { group = "androidx.compose.ui", name="ui", version.ref = "androidx-compose" }
32 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
33 | androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" }
34 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
35 | androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
36 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" }
37 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
38 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
39 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
40 | libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
41 | libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" }
42 | libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" }
43 | libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }
44 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
45 | okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
46 | androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "ui-tooling-preview-android" }
47 | androidx-animation-core-android = { group = "androidx.compose.animation", name = "animation-core-android", version.ref = "animation-core-android" }
48 |
49 | [plugins]
50 | android-application = { id = "com.android.application", version.ref = "android-application" }
51 | devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" }
52 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
53 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
54 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.slot
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.ColumnScope
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.res.stringResource
15 | import androidx.compose.ui.unit.ExperimentalUnitApi
16 | import androidx.compose.ui.unit.dp
17 | import androidx.navigation.NavController
18 | import com.github.capntrips.kernelflasher.R
19 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
20 | import com.github.capntrips.kernelflasher.ui.components.SlotCard
21 |
22 | @ExperimentalAnimationApi
23 | @ExperimentalMaterial3Api
24 | @ExperimentalUnitApi
25 | @Composable
26 | fun ColumnScope.SlotContent(
27 | viewModel: SlotViewModel,
28 | slotSuffix: String,
29 | navController: NavController
30 | ) {
31 | val context = LocalContext.current
32 | SlotCard(
33 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot),
34 | viewModel = viewModel,
35 | navController = navController,
36 | isSlotScreen = true
37 | )
38 | AnimatedVisibility(!viewModel.isRefreshing.value) {
39 | Column {
40 | Spacer(Modifier.height(5.dp))
41 | MyOutlinedButton(
42 | {
43 | navController.navigate("slot$slotSuffix/flash")
44 | }
45 | ) {
46 | Text(stringResource(R.string.flash))
47 | }
48 | MyOutlinedButton(
49 | {
50 | viewModel.clearFlash(context)
51 | navController.navigate("slot$slotSuffix/backup")
52 | }
53 | ) {
54 | Text(stringResource(R.string.backup))
55 | }
56 | MyOutlinedButton(
57 | {
58 | navController.navigate("slot$slotSuffix/backups")
59 | }
60 | ) {
61 | Text(stringResource(R.string.restore))
62 | }
63 | MyOutlinedButton(
64 | { if (!viewModel.isRefreshing.value) viewModel.getKernel(context) }
65 | ) {
66 | Text(stringResource(R.string.check_kernel_version))
67 | }
68 | if (viewModel.hasVendorDlkm) {
69 | AnimatedVisibility(!viewModel.isRefreshing.value) {
70 | AnimatedVisibility(viewModel.isVendorDlkmMounted) {
71 | MyOutlinedButton(
72 | { viewModel.unmountVendorDlkm(context) }
73 | ) {
74 | Text(stringResource(R.string.unmount_vendor_dlkm))
75 | }
76 | }
77 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) {
78 | Column {
79 | MyOutlinedButton(
80 | { viewModel.mountVendorDlkm(context) }
81 | ) {
82 | Text(stringResource(R.string.mount_vendor_dlkm))
83 | }
84 | MyOutlinedButton(
85 | { viewModel.unmapVendorDlkm(context) }
86 | ) {
87 | Text(stringResource(R.string.unmap_vendor_dlkm))
88 | }
89 | }
90 | }
91 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) {
92 | MyOutlinedButton(
93 | { viewModel.mapVendorDlkm(context) }
94 | ) {
95 | Text(stringResource(R.string.map_vendor_dlkm))
96 | }
97 | }
98 | }
99 | }
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.main
2 |
3 | import android.os.Build
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.height
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.mutableIntStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.unit.dp
17 | import androidx.navigation.NavController
18 | import com.github.capntrips.kernelflasher.R
19 | import com.github.capntrips.kernelflasher.ui.components.DataCard
20 | import com.github.capntrips.kernelflasher.ui.components.DataRow
21 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
22 | import com.github.capntrips.kernelflasher.ui.components.SlotCard
23 | import kotlinx.serialization.ExperimentalSerializationApi
24 |
25 | @ExperimentalMaterial3Api
26 | @ExperimentalSerializationApi
27 | @Composable
28 | fun ColumnScope.MainContent(
29 | viewModel: MainViewModel,
30 | navController: NavController
31 | ) {
32 | val context = LocalContext.current
33 | DataCard(title = stringResource(R.string.device)) {
34 | val cardWidth = remember { mutableIntStateOf(0) }
35 | DataRow(
36 | stringResource(R.string.model),
37 | "${Build.MODEL} (${Build.DEVICE})",
38 | mutableMaxWidth = cardWidth
39 | )
40 | DataRow(stringResource(R.string.build_number), Build.ID, mutableMaxWidth = cardWidth)
41 | DataRow(
42 | stringResource(R.string.kernel_version),
43 | viewModel.kernelVersion,
44 | mutableMaxWidth = cardWidth,
45 | clickable = true
46 | )
47 | if (viewModel.isAb) {
48 | DataRow(
49 | stringResource(R.string.slot_suffix),
50 | viewModel.slotSuffix,
51 | mutableMaxWidth = cardWidth
52 | )
53 | }
54 | if (viewModel.susfsVersion != "v0.0.0" && viewModel.susfsVersion != "Invalid") {
55 | DataRow(
56 | stringResource(R.string.susfs_version),
57 | viewModel.susfsVersion,
58 | mutableMaxWidth = cardWidth
59 | )
60 | }
61 | }
62 | Spacer(Modifier.height(16.dp))
63 | SlotCard(
64 | title = stringResource(if (viewModel.isAb) R.string.slot_a else R.string.slot),
65 | viewModel = viewModel.slotA,
66 | navController = navController
67 | )
68 | if (viewModel.isAb && viewModel.slotB?.hasError == false) {
69 | Spacer(Modifier.height(16.dp))
70 | SlotCard(
71 | title = stringResource(R.string.slot_b),
72 | viewModel = viewModel.slotB,
73 | navController = navController
74 | )
75 | }
76 | Spacer(Modifier.height(16.dp))
77 | AnimatedVisibility(!viewModel.isRefreshing) {
78 | MyOutlinedButton(
79 | onclick = { navController.navigate("backups") }
80 | ) {
81 | Text(stringResource(R.string.backups))
82 | }
83 | }
84 | if (viewModel.hasRamoops) {
85 | MyOutlinedButton(
86 | onclick = { viewModel.saveRamoops(context) }
87 | ) {
88 | Text(stringResource(R.string.save_ramoops))
89 | }
90 | }
91 | MyOutlinedButton(
92 | onclick = { viewModel.saveDmesg(context) }
93 | ) {
94 | Text(stringResource(R.string.save_dmesg))
95 | }
96 | MyOutlinedButton(
97 | onclick = { viewModel.saveLogcat(context) }
98 | ) {
99 | Text(stringResource(R.string.save_logcat))
100 | }
101 | AnimatedVisibility(!viewModel.isRefreshing) {
102 | MyOutlinedButton(
103 | onclick = { navController.navigate("reboot") }
104 | ) {
105 | Text(stringResource(R.string.reboot))
106 | }
107 | }
108 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Kernel Flasher
3 | Root is required
4 | Root service disconnected
5 | Device
6 | Model
7 | Build Number
8 | Kernel Name
9 | Kernel Version
10 | Slot Suffix
11 | Slot
12 | Slot A
13 | Slot B
14 | Boot SHA1
15 | Vendor DLKM
16 | Exists
17 | Not Found
18 | Mounted
19 | Unmounted
20 | View
21 | Backups
22 | Save ramoops
23 | Save dmesg
24 | Save logcat
25 | Back
26 | Backup
27 | Updates
28 | Flash
29 | Flash using mkbootfs
30 | Flash AK3 Zip
31 | Flash AK3 Zip using mkbootfs
32 | Flash Partition Image
33 | Flash KernelSU LKM Driver
34 | Restore
35 | Check Kernel Version
36 | Mount Vendor DLKM
37 | Unmount Vendor DLKM
38 | Map Vendor DLKM
39 | Unmap Vendor DLKM
40 | Migrate
41 | No backups found
42 | Delete
43 | Add
44 | URL
45 | Version
46 | Date Released
47 | Last Updated
48 | Changelog
49 | Check for Updates
50 | Download
51 | Reboot
52 | Soft Reboot
53 | Reboot to Recovery
54 | Reboot to Bootloader
55 | Reboot to Download
56 | Reboot to EDL
57 | Save AK3 Log
58 | Save Flash Log
59 | Save Backup Log
60 | Save Restore Log
61 | Save AK3 Zip as Backup
62 | Backup Type
63 | Hashes
64 | Partition selection unavailable for legacy backups
65 | boot.img Format
66 | init_boot.img Format
67 | Ramdisk Format
68 | SUSFS Version
69 | unsupported
70 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kernel Flasher
4 | Root é necessário
5 | Serviço root desconectado
6 | Dispositivo
7 | Modelo
8 | Número da versão
9 | Nome do kernel
10 | Versão do kernel
11 | Sufixo de slot
12 | Slot A
13 | Slot B
14 | Boot SHA1
15 | Vendor DLKM
16 | Existe
17 | Não encontrado
18 | Montado
19 | Desmontado
20 | Visualizar
21 | Backups
22 | Salvar ramoops
23 | Salvar dmesg
24 | Salvar logcat
25 | Voltar
26 | Backup
27 | Atualizações
28 | Flash
29 | Flash using mkbootfs
30 | Flash AK3 ZIP
31 | Flash AK3 Zip using mkbootfs
32 | Flashar imagem de partição
33 | Flash KernelSU LKM Driver
34 | Restaurar
35 | Verificar versão do kernel
36 | Montar Vendor DLKM
37 | Desmontar Vendor DLKM
38 | Mapear Vendor DLKM
39 | Desmapear Vendor DLKM
40 | Migrar
41 | Nenhum backup encontrado
42 | Excluir
43 | Adicionar
44 | URL
45 | Versão
46 | Data de lançamento
47 | Ultima atualização
48 | Registro de alterações
49 | Verificar por atualizações
50 | Baixar
51 | Reiniciar
52 | Reinicialização suave
53 | Reiniciar em modo Recovery
54 | Reiniciar em modo Bootloader
55 | Reiniciar em modo Download
56 | Reiniciar em modo EDL
57 | Salvar registro do AK3
58 | Salvar registro do flash
59 | Salvar registro do backup
60 | Salvar registro da restauração
61 | Salvar AK3 ZIP como backup
62 | Tipo de backup
63 | Hashes
64 | Seleção de partição indisponível para backups antigos
65 | Formato do boot.img
66 | Formato do init_boot.img
67 | Formato do Ramdisk
68 | SUSFS Versão
69 | unsupported
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/values-ru/string.xml:
--------------------------------------------------------------------------------
1 |
2 | Kernel Flasher
3 | Требуется root-доступ
4 | Служба root отключена
5 | Устройство
6 | Модель
7 | Номер сборки
8 | Kernel Name
9 | Версия ядра
10 | Slot Suffix
11 | Слот
12 | Слот A
13 | Слот B
14 | Boot SHA1
15 | Vendor DLKM
16 | Существует
17 | Не найдено
18 | Подключено
19 | Отключено
20 | Просмотр
21 | Резервные копии
22 | Сохранить ramoops
23 | Сохранить dmesg
24 | Сохранить logcat
25 | Назад
26 | Резервное копирование
27 | Обновления
28 | Прошивка
29 | Прошить помощью mkbootfs
30 | Прошить AK3 Zip
31 | Прошить AK3 Zip с помощью mkbootfs
32 | Прошить образ раздела
33 | Прошивка KernelSU LKM Driver
34 | Восстановить
35 | Проверить версию ядра
36 | Подключить Vendor DLKM
37 | Отключить Vendor DLKM
38 | Сопоставить Vendor DLKM
39 | Отменить сопоставление Vendor DLKM
40 | Мигрировать
41 | Резервные копии не найдены
42 | Удалить
43 | Добавить
44 | URL
45 | Версия
46 | Дата выпуска
47 | Последнее обновление
48 | Список изменений
49 | Проверить обновления
50 | Скачать
51 | Перезагрузить
52 | Soft Reboot
53 | Reboot to Recovery
54 | Reboot to Bootloader
55 | Reboot to Download Mode
56 | Reboot to EDL
57 | Сохранить журнал AK3
58 | Сохранить журнал прошивки
59 | Сохранить журнал резервного копирования
60 | Сохранить журнал восстановления
61 | Сохранить AK3 Zip как резервную копию
62 | Тип резервной копии
63 | Хеши
64 | Выбор раздела недоступен для устаревших резервных копий
65 | boot.img формат
66 | init_boot.img формат
67 | Ramdisk формат
68 | SUSFS Версия
69 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/values-pl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Kernel Flasher
4 | Wymagane uprawnienia Superużytkownika
5 | Usługa Superużytkownika została odłączona
6 | Urządzenie
7 | Model
8 | Kompilacja
9 | Nazwa jądra
10 | Wersja jądra
11 | Przyrostek slotu
12 | Slot
13 | Slot A
14 | Slot B
15 | Boot SHA1
16 | Vendor DLKM
17 | Istnieje
18 | Nie znaleziono
19 | Zamontowany
20 | Niezamontowany
21 | Wybierz
22 | Kopie zapasowe
23 | Zapisz ramoops
24 | Zapisz dmesg
25 | Zapisz logcat
26 | Wstecz
27 | Utwórz kopię zapasową
28 | Aktualizacje
29 | Sflashuj
30 | Sflashuj przy użyciu mkbootfs
31 | Sflashuj archiwum AK3
32 | Flashuj archiwum AK3 za pomocą mkbootfs
33 | Sflashuj obraz partycji
34 | Sflashuj KernelSU LKM Driver
35 | Przywróć
36 | Sprawdź wersję jądra
37 | Zamontuj Vendor DLKM
38 | Odmontuj Vendor DLKM
39 | Migruj
40 | Zmapuj Vendor DLKM
41 | Odmapuj Vendor DLKM
42 | Nie znaleziono kopii zapasowych
43 | Usuń
44 | Dodaj
45 | Adres URL
46 | Wersja
47 | Data publikacji
48 | Ostatnia aktualizacja
49 | Lista zmian
50 | Sprawdź aktualizacje
51 | Pobierz
52 | Uruchom ponownie
53 | Miękki restart
54 | Uruchom ponownie do trybu Recovery
55 | Uruchom ponownie do trybu Bootloader
56 | Uruchom ponownie do trybu Download
57 | Uruchom ponownie do trybu EDL
58 | Zapisz dziennik AK3
59 | Zapisz dziennik Flashowania
60 | Zapisz dziennik kopii zapasowej
61 | Zapisz dziennik przywracania
62 | Zapisz archiwum AK3 jako kopię zapasową
63 | Typ kopii zapasowej
64 | Sumy kontrolne
65 | Wybór partycji niedostępny dla kopii zapasowych starszego formatu
66 | Format boot.img
67 | Format init_boot.img
68 | Format Ramdisk
69 | Wersja SUSFS
70 | unsupported
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.components
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.interaction.collectIsDraggedAsState
6 | import androidx.compose.foundation.layout.ColumnScope
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.foundation.lazy.LazyListState
12 | import androidx.compose.foundation.lazy.items
13 | import androidx.compose.foundation.lazy.rememberLazyListState
14 | import androidx.compose.material3.LocalTextStyle
15 | import androidx.compose.material3.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.runtime.LaunchedEffect
18 | import androidx.compose.runtime.getValue
19 | import androidx.compose.runtime.mutableIntStateOf
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.remember
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.ui.Modifier
24 | import androidx.compose.ui.composed
25 | import androidx.compose.ui.draw.drawWithContent
26 | import androidx.compose.ui.geometry.CornerRadius
27 | import androidx.compose.ui.geometry.Offset
28 | import androidx.compose.ui.geometry.Size
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.text.font.FontFamily
31 | import androidx.compose.ui.unit.Dp
32 | import androidx.compose.ui.unit.ExperimentalUnitApi
33 | import androidx.compose.ui.unit.TextUnit
34 | import androidx.compose.ui.unit.TextUnitType
35 | import androidx.compose.ui.unit.dp
36 |
37 | @ExperimentalUnitApi
38 | @Composable
39 | fun ColumnScope.FlashList(
40 | cardTitle: String,
41 | output: List,
42 | content: @Composable ColumnScope.() -> Unit
43 | ) {
44 | val listState = rememberLazyListState()
45 | var hasDragged by remember { mutableStateOf(false) }
46 | val isDragged by listState.interactionSource.collectIsDraggedAsState()
47 | if (isDragged) {
48 | hasDragged = true
49 | }
50 | var shouldScroll = false
51 | if (!hasDragged) {
52 | if (listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != null) {
53 | if (listState.layoutInfo.totalItemsCount - listState.layoutInfo.visibleItemsInfo.size > listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index!!) {
54 | shouldScroll = true
55 | }
56 | }
57 | }
58 | LaunchedEffect(shouldScroll) {
59 | listState.animateScrollToItem(output.size)
60 | }
61 | DataCard(cardTitle)
62 | Spacer(Modifier.height(4.dp))
63 | LazyColumn(
64 | Modifier
65 | .weight(1.0f)
66 | .fillMaxSize()
67 | .scrollbar(listState),
68 | listState
69 | ) {
70 | items(output) { message ->
71 | Text(
72 | message,
73 | style = LocalTextStyle.current.copy(
74 | fontFamily = FontFamily.Monospace,
75 | fontSize = TextUnit(12.0f, TextUnitType.Sp),
76 | lineHeight = TextUnit(18.0f, TextUnitType.Sp)
77 | )
78 | )
79 | }
80 | }
81 | content()
82 | }
83 |
84 | // https://stackoverflow.com/a/68056586/434343
85 | fun Modifier.scrollbar(
86 | state: LazyListState,
87 | width: Dp = 6.dp
88 | ): Modifier = composed {
89 | var visibleItemsCountChanged = false
90 | var visibleItemsCount by remember { mutableIntStateOf(state.layoutInfo.visibleItemsInfo.size) }
91 | if (visibleItemsCount != state.layoutInfo.visibleItemsInfo.size) {
92 | visibleItemsCountChanged = true
93 | visibleItemsCount = state.layoutInfo.visibleItemsInfo.size
94 | }
95 |
96 | val hidden = state.layoutInfo.visibleItemsInfo.size == state.layoutInfo.totalItemsCount
97 | val targetAlpha =
98 | if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0.5f else 0f
99 | val delay = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0 else 250
100 | val duration =
101 | if (hidden || visibleItemsCountChanged) 0 else if (state.isScrollInProgress) 150 else 500
102 |
103 | val alpha by animateFloatAsState(
104 | targetValue = targetAlpha,
105 | animationSpec = tween(delayMillis = delay, durationMillis = duration)
106 | )
107 |
108 | drawWithContent {
109 | drawContent()
110 |
111 | val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index
112 | val needDrawScrollbar = state.isScrollInProgress || visibleItemsCountChanged || alpha > 0.0f
113 |
114 | if (needDrawScrollbar && firstVisibleElementIndex != null) {
115 | val elementHeight = this.size.height / state.layoutInfo.totalItemsCount
116 | val scrollbarOffsetY = firstVisibleElementIndex * elementHeight
117 | val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight
118 |
119 | drawRoundRect(
120 | color = Color.Gray,
121 | topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY),
122 | size = Size(width.toPx(), scrollbarHeight),
123 | cornerRadius = CornerRadius(width.toPx(), width.toPx()),
124 | alpha = alpha
125 | )
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.fadeIn
5 | import androidx.compose.animation.fadeOut
6 | import androidx.compose.foundation.isSystemInDarkTheme
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.ColumnScope
10 | import androidx.compose.foundation.layout.WindowInsets
11 | import androidx.compose.foundation.layout.WindowInsetsSides
12 | import androidx.compose.foundation.layout.asPaddingValues
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.navigationBars
16 | import androidx.compose.foundation.layout.only
17 | import androidx.compose.foundation.layout.padding
18 | import androidx.compose.foundation.layout.statusBars
19 | import androidx.compose.foundation.rememberScrollState
20 | import androidx.compose.foundation.verticalScroll
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.filled.ArrowBack
23 | import androidx.compose.material3.ExperimentalMaterial3Api
24 | import androidx.compose.material3.Icon
25 | import androidx.compose.material3.IconButton
26 | import androidx.compose.material3.MaterialTheme
27 | import androidx.compose.material3.Scaffold
28 | import androidx.compose.material3.Text
29 | import androidx.compose.material3.pulltorefresh.PullToRefreshBox
30 | import androidx.compose.runtime.Composable
31 | import androidx.compose.ui.Alignment
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.graphics.Brush
34 | import androidx.compose.ui.graphics.Color
35 | import androidx.compose.ui.platform.LocalContext
36 | import androidx.compose.ui.res.stringResource
37 | import androidx.compose.ui.text.SpanStyle
38 | import androidx.compose.ui.text.buildAnnotatedString
39 | import androidx.compose.ui.text.font.FontWeight
40 | import androidx.compose.ui.text.withStyle
41 | import androidx.compose.ui.unit.dp
42 | import androidx.compose.ui.unit.sp
43 | import androidx.navigation.NavController
44 | import com.github.capntrips.kernelflasher.R
45 | import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel
46 | import kotlinx.serialization.ExperimentalSerializationApi
47 |
48 | val colorList: List = listOf(
49 | Color(0xE5E57373),
50 | Color(0xE564B5F6),
51 | Color(0xE54DB6AC),
52 | Color(0xE581C784),
53 | Color(0xE5FFD54F),
54 | Color(0xE5FF8A65),
55 | Color(0xE5A1887F),
56 | Color(0xE590A4AE)
57 | ).shuffled()
58 |
59 |
60 | @ExperimentalMaterial3Api
61 | @ExperimentalSerializationApi
62 | @Composable
63 | fun RefreshableScreen(
64 | viewModel: MainViewModel,
65 | navController: NavController,
66 | swipeEnabled: Boolean = false,
67 | content: @Composable ColumnScope.() -> Unit
68 | ) {
69 | val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues()
70 | val navigationBars = WindowInsets.navigationBars.asPaddingValues()
71 | val context = LocalContext.current
72 |
73 | Scaffold(
74 | topBar = {
75 | Box(
76 | Modifier
77 | .fillMaxWidth()
78 | .padding(statusBar)
79 | ) {
80 | if (navController.previousBackStackEntry != null) {
81 | AnimatedVisibility(
82 | !viewModel.isRefreshing,
83 | enter = fadeIn(),
84 | exit = fadeOut()
85 | ) {
86 | IconButton(
87 | onClick = { navController.popBackStack() },
88 | modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp)
89 | ) {
90 | Icon(
91 | Icons.Filled.ArrowBack,
92 | contentDescription = stringResource(R.string.back),
93 | tint = MaterialTheme.colorScheme.onSurface
94 | )
95 | }
96 | }
97 | }
98 | Box(
99 | Modifier
100 | .fillMaxWidth()
101 | .padding(vertical = 14.dp, horizontal = 2.dp)
102 | ) {
103 | if (isSystemInDarkTheme()) {
104 | Text(
105 | modifier = Modifier
106 | .align(Alignment.Center)
107 | .padding(bottom = 8.dp),
108 | text = buildAnnotatedString {
109 | withStyle(
110 | style = SpanStyle(brush = Brush.linearGradient(colors = colorList))
111 | ) {
112 | append("Qkernel Flasher")
113 | }
114 | },
115 | style = MaterialTheme.typography.headlineMedium,
116 | fontWeight = FontWeight.Medium,
117 | fontSize = 23.5.sp
118 | )
119 | } else {
120 | Text(
121 | modifier = Modifier
122 | .align(Alignment.Center)
123 | .padding(bottom = 8.dp),
124 | text = "Qkernel Flasher",
125 | style = MaterialTheme.typography.headlineMedium,
126 | fontWeight = FontWeight.Medium,
127 | color = MaterialTheme.colorScheme.onSurface,
128 | fontSize = 23.5.sp
129 | )
130 | }
131 | }
132 | }
133 | }
134 | ) { paddingValues ->
135 | PullToRefreshBox(
136 | isRefreshing = viewModel.isRefreshing,
137 | onRefresh = { viewModel.refresh(context) },
138 | modifier = Modifier
139 | .padding(paddingValues)
140 | .fillMaxSize()
141 | ) {
142 | Column(
143 | modifier = Modifier
144 | .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding())
145 | .fillMaxSize()
146 | .verticalScroll(rememberScrollState()),
147 | content = content
148 | )
149 | }
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.backups
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.material3.ExperimentalMaterial3Api
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.mutableIntStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.LocalContext
17 | import androidx.compose.ui.res.stringResource
18 | import androidx.compose.ui.text.font.FontFamily
19 | import androidx.compose.ui.text.font.FontStyle
20 | import androidx.compose.ui.text.font.FontWeight
21 | import androidx.compose.ui.text.style.TextAlign
22 | import androidx.compose.ui.unit.dp
23 | import androidx.navigation.NavController
24 | import com.github.capntrips.kernelflasher.R
25 | import com.github.capntrips.kernelflasher.common.PartitionUtil
26 | import com.github.capntrips.kernelflasher.ui.components.DataCard
27 | import com.github.capntrips.kernelflasher.ui.components.DataRow
28 | import com.github.capntrips.kernelflasher.ui.components.DataSet
29 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
30 | import com.github.capntrips.kernelflasher.ui.components.ViewButton
31 |
32 | @ExperimentalMaterial3Api
33 | @Composable
34 | fun ColumnScope.BackupsContent(
35 | viewModel: BackupsViewModel,
36 | navController: NavController
37 | ) {
38 | val context = LocalContext.current
39 | if (viewModel.currentBackup != null && viewModel.backups.containsKey(viewModel.currentBackup)) {
40 | DataCard(viewModel.currentBackup!!) {
41 | val cardWidth = remember { mutableIntStateOf(0) }
42 | val currentBackup = viewModel.backups.getValue(viewModel.currentBackup!!)
43 | DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth)
44 | DataRow(
45 | stringResource(R.string.kernel_version),
46 | currentBackup.kernelVersion,
47 | mutableMaxWidth = cardWidth,
48 | clickable = true
49 | )
50 | if (currentBackup.type == "raw") {
51 | DataRow(
52 | label = stringResource(R.string.boot_sha1),
53 | value = currentBackup.bootSha1!!.substring(0, 8),
54 | valueStyle = MaterialTheme.typography.titleSmall.copy(
55 | fontFamily = FontFamily.Monospace,
56 | fontWeight = FontWeight.Medium
57 | ),
58 | mutableMaxWidth = cardWidth
59 | )
60 | if (currentBackup.hashes != null) {
61 | val hashWidth = remember { mutableIntStateOf(0) }
62 | DataSet(stringResource(R.string.hashes)) {
63 | for (partitionName in PartitionUtil.PartitionNames) {
64 | val hash = currentBackup.hashes.get(partitionName)
65 | if (hash != null) {
66 | DataRow(
67 | label = partitionName,
68 | value = hash.substring(0, 8),
69 | valueStyle = MaterialTheme.typography.titleSmall.copy(
70 | fontFamily = FontFamily.Monospace,
71 | fontWeight = FontWeight.Medium
72 | ),
73 | mutableMaxWidth = hashWidth
74 | )
75 | }
76 | }
77 | }
78 | }
79 | }
80 | }
81 | AnimatedVisibility(!viewModel.isRefreshing) {
82 | Column {
83 | Spacer(Modifier.height(5.dp))
84 | MyOutlinedButton(
85 | onclick = { viewModel.delete(context) { navController.popBackStack() } }
86 | ) {
87 | Text(stringResource(R.string.delete))
88 | }
89 | }
90 | }
91 | } else {
92 | DataCard(stringResource(R.string.backups))
93 | AnimatedVisibility(false) {
94 | Column {
95 | Spacer(Modifier.height(5.dp))
96 | MyOutlinedButton(
97 | onclick = { }
98 | ) {
99 | Text(stringResource(R.string.migrate))
100 | }
101 | }
102 | }
103 | if (viewModel.backups.isNotEmpty()) {
104 | for (id in viewModel.backups.keys.sortedByDescending { it }) {
105 | val currentBackup = viewModel.backups[id]!!
106 | Spacer(Modifier.height(16.dp))
107 | DataCard(
108 | title = id,
109 | button = {
110 | AnimatedVisibility(!viewModel.isRefreshing) {
111 | Column {
112 | ViewButton(onClick = {
113 | navController.navigate("backups/$id")
114 | })
115 | }
116 | }
117 | }
118 | ) {
119 | val cardWidth = remember { mutableIntStateOf(0) }
120 | if (currentBackup.type == "raw") {
121 | DataRow(
122 | label = stringResource(R.string.boot_sha1),
123 | value = currentBackup.bootSha1!!.substring(0, 8),
124 | valueStyle = MaterialTheme.typography.titleSmall.copy(
125 | fontFamily = FontFamily.Monospace,
126 | fontWeight = FontWeight.Medium
127 | ),
128 | mutableMaxWidth = cardWidth
129 | )
130 | }
131 | DataRow(
132 | stringResource(R.string.kernel_version),
133 | currentBackup.kernelVersion,
134 | mutableMaxWidth = cardWidth,
135 | clickable = true
136 | )
137 | }
138 | }
139 | } else {
140 | Spacer(Modifier.height(32.dp))
141 | Text(
142 | stringResource(R.string.no_backups_found),
143 | modifier = Modifier.fillMaxWidth(),
144 | textAlign = TextAlign.Center,
145 | fontStyle = FontStyle.Italic
146 | )
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.main
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.Log
6 | import android.widget.Toast
7 | import androidx.compose.runtime.MutableState
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.lifecycle.ViewModel
10 | import androidx.lifecycle.viewModelScope
11 | import androidx.navigation.NavController
12 | import com.github.capntrips.kernelflasher.common.PartitionUtil
13 | import com.github.capntrips.kernelflasher.common.types.backups.Backup
14 | import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsViewModel
15 | import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootViewModel
16 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel
17 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewModel
18 | import com.topjohnwu.superuser.Shell
19 | import com.topjohnwu.superuser.nio.FileSystemManager
20 | import kotlinx.coroutines.Dispatchers
21 | import kotlinx.coroutines.launch
22 | import kotlinx.coroutines.withContext
23 | import kotlinx.serialization.ExperimentalSerializationApi
24 | import java.io.File
25 | import java.time.LocalDateTime
26 | import java.time.format.DateTimeFormatter
27 |
28 | @ExperimentalSerializationApi
29 | class MainViewModel(
30 | context: Context,
31 | fileSystemManager: FileSystemManager,
32 | private val navController: NavController
33 | ) : ViewModel() {
34 | companion object {
35 | const val TAG: String = "KernelFlasher/MainViewModel"
36 | }
37 |
38 | val slotSuffix: String
39 |
40 | val kernelVersion: String
41 | val susfsVersion: String
42 | val isAb: Boolean
43 | val slotA: SlotViewModel
44 | val slotB: SlotViewModel?
45 | val backups: BackupsViewModel
46 | val updates: UpdatesViewModel
47 | val reboot: RebootViewModel
48 | val hasRamoops: Boolean
49 |
50 | private val _isRefreshing: MutableState = mutableStateOf(true)
51 | private var _error: String? = null
52 | private var _backups: MutableMap = mutableMapOf()
53 |
54 | val isRefreshing: Boolean
55 | get() = _isRefreshing.value
56 | val hasError: Boolean
57 | get() = _error != null
58 | val error: String
59 | get() = _error!!
60 |
61 | data class UpdateDialogData(
62 | val title: String,
63 | val changelog: List,
64 | val onConfirm: () -> Unit
65 | )
66 |
67 | init {
68 | PartitionUtil.init(context, fileSystemManager)
69 | kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0]
70 | susfsVersion = runCatching { Shell.cmd("susfsd version").exec().out[0] }
71 | .recoverCatching { Shell.cmd("ksu_susfs show version").exec().out[0] }
72 | .getOrDefault("v0.0.0")
73 | slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0]
74 | backups = BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups)
75 | updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing)
76 | reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing)
77 | // https://cs.android.com/android/platform/superproject/+/android-14.0.0_r18:bootable/recovery/recovery.cpp;l=320
78 | isAb = slotSuffix.isNotEmpty()
79 | if (isAb) {
80 | val bootA = PartitionUtil.findPartitionBlockDevice(context, "boot", "_a")!!
81 | val bootB = PartitionUtil.findPartitionBlockDevice(context, "boot", "_b")!!
82 | val initBootA = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_a")
83 | val initBootB = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_b")
84 | slotA = SlotViewModel(
85 | context,
86 | fileSystemManager,
87 | navController,
88 | _isRefreshing,
89 | slotSuffix == "_a",
90 | "_a",
91 | bootA,
92 | initBootA,
93 | _backups
94 | )
95 | slotB = SlotViewModel(
96 | context,
97 | fileSystemManager,
98 | navController,
99 | _isRefreshing,
100 | slotSuffix == "_b",
101 | "_b",
102 | bootB,
103 | initBootB,
104 | _backups
105 | )
106 | } else {
107 | val boot = PartitionUtil.findPartitionBlockDevice(context, "boot", "")!!
108 | val initBoot = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "")
109 | slotA = SlotViewModel(
110 | context,
111 | fileSystemManager,
112 | navController,
113 | _isRefreshing,
114 | true,
115 | "",
116 | boot,
117 | initBoot,
118 | _backups
119 | )
120 | if (slotA.hasError) {
121 | _error = slotA.error
122 | }
123 | slotB = null
124 | }
125 |
126 | hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists()
127 | _isRefreshing.value = false
128 | }
129 |
130 | fun refresh(context: Context) {
131 | launch {
132 | slotA.refresh(context)
133 | if (isAb) {
134 | slotB!!.refresh(context)
135 | }
136 | backups.refresh(context)
137 | }
138 | }
139 |
140 | private fun launch(block: suspend () -> Unit) {
141 | viewModelScope.launch {
142 | _isRefreshing.value = true
143 | try {
144 | withContext(Dispatchers.IO) {
145 | block()
146 | }
147 | } catch (e: Exception) {
148 | Log.e(TAG, e.message, e)
149 | navController.navigate("error/${e.message}") {
150 | popUpTo("main")
151 | }
152 | }
153 | _isRefreshing.value = false
154 | }
155 | }
156 |
157 | @Suppress("SameParameterValue")
158 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
159 | Log.d(TAG, message)
160 | if (!shouldThrow) {
161 | viewModelScope.launch(Dispatchers.Main) {
162 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
163 | }
164 | } else {
165 | throw Exception(message)
166 | }
167 | }
168 |
169 | fun saveRamoops(context: Context) {
170 | launch {
171 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
172 |
173 | @SuppressLint("SdCardPath")
174 | val ramoops = File("/sdcard/Download/console-ramoops--$now.log")
175 | Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec()
176 | if (ramoops.exists()) {
177 | log(context, "Saved ramoops to $ramoops")
178 | } else {
179 | log(context, "Failed to save $ramoops", shouldThrow = true)
180 | }
181 | }
182 | }
183 |
184 | fun saveDmesg(context: Context) {
185 | launch {
186 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
187 |
188 | @SuppressLint("SdCardPath")
189 | val dmesg = File("/sdcard/Download/dmesg--$now.log")
190 | Shell.cmd("dmesg > $dmesg").exec()
191 | if (dmesg.exists()) {
192 | log(context, "Saved dmesg to $dmesg")
193 | } else {
194 | log(context, "Failed to save $dmesg", shouldThrow = true)
195 | }
196 | }
197 | }
198 |
199 | fun saveLogcat(context: Context) {
200 | launch {
201 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
202 |
203 | @SuppressLint("SdCardPath")
204 | val logcat = File("/sdcard/Download/logcat--$now.log")
205 | Shell.cmd("logcat -d > $logcat").exec()
206 | if (logcat.exists()) {
207 | log(context, "Saved logcat to $logcat")
208 | } else {
209 | log(context, "Failed to save $logcat", shouldThrow = true)
210 | }
211 | }
212 | }
213 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.updates
2 |
3 | import android.content.ContentValues
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.os.Environment
7 | import android.provider.MediaStore
8 | import android.util.Log
9 | import android.widget.Toast
10 | import androidx.compose.runtime.MutableState
11 | import androidx.compose.runtime.mutableStateListOf
12 | import androidx.compose.runtime.snapshots.SnapshotStateList
13 | import androidx.lifecycle.ViewModel
14 | import androidx.lifecycle.viewModelScope
15 | import androidx.navigation.NavController
16 | import androidx.room.Room
17 | import com.github.capntrips.kernelflasher.common.types.room.AppDatabase
18 | import com.github.capntrips.kernelflasher.common.types.room.updates.Update
19 | import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateSerializer
20 | import com.topjohnwu.superuser.nio.FileSystemManager
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.launch
23 | import kotlinx.coroutines.withContext
24 | import kotlinx.serialization.ExperimentalSerializationApi
25 | import kotlinx.serialization.json.Json
26 | import okhttp3.OkHttpClient
27 | import okhttp3.Request
28 | import java.io.IOException
29 | import java.text.SimpleDateFormat
30 | import java.util.Date
31 | import java.util.Locale
32 | import kotlin.io.path.Path
33 | import kotlin.io.path.name
34 |
35 | @ExperimentalSerializationApi
36 | class UpdatesViewModel(
37 | context: Context,
38 | @Suppress("unused") private val fileSystemManager: FileSystemManager,
39 | private val navController: NavController,
40 | private val _isRefreshing: MutableState
41 | ) : ViewModel() {
42 | companion object {
43 | const val TAG: String = "KernelFlasher/UpdatesState"
44 | val lastUpdatedFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US)
45 | }
46 |
47 | private val client = OkHttpClient()
48 | private val db = Room.databaseBuilder(context, AppDatabase::class.java, "kernel-flasher").build()
49 | private val updateDao = db.updateDao()
50 | private val _updates: SnapshotStateList = mutableStateListOf()
51 |
52 | var currentUpdate: Update? = null
53 | var changelog: String? = null
54 |
55 | val updates: List
56 | get() = _updates
57 | val isRefreshing: Boolean
58 | get() = _isRefreshing.value
59 |
60 | init {
61 | launch {
62 | val updates = updateDao.getAll()
63 | viewModelScope.launch(Dispatchers.Main) {
64 | _updates.addAll(updates)
65 | }
66 | }
67 | }
68 |
69 | private fun launch(block: suspend () -> Unit) {
70 | viewModelScope.launch(Dispatchers.IO) {
71 | viewModelScope.launch(Dispatchers.Main) {
72 | _isRefreshing.value = true
73 | }
74 | try {
75 | block()
76 | } catch (e: Exception) {
77 | withContext(Dispatchers.Main) {
78 | Log.e(TAG, e.message, e)
79 | navController.navigate("error/${e.message}") {
80 | popUpTo("main")
81 | }
82 | }
83 | }
84 | viewModelScope.launch(Dispatchers.Main) {
85 | _isRefreshing.value = false
86 | }
87 | }
88 | }
89 |
90 | @Suppress("SameParameterValue")
91 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
92 | Log.d(TAG, message)
93 | if (!shouldThrow) {
94 | viewModelScope.launch(Dispatchers.Main) {
95 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
96 | }
97 | } else {
98 | throw Exception(message)
99 | }
100 | }
101 |
102 | fun clearCurrent() {
103 | currentUpdate = null
104 | changelog = null
105 | }
106 |
107 | fun add(url: String, callback: (updateId: Int) -> Unit) {
108 | launch {
109 | val request = Request.Builder()
110 | .url(url)
111 | .build()
112 |
113 | client.newCall(request).execute().use { response ->
114 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
115 | val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string())
116 | update.updateUri = url
117 | update.lastUpdated = Date()
118 | val updateId = updateDao.insert(update).toInt()
119 | val inserted = updateDao.load(updateId)
120 | withContext(Dispatchers.Main) {
121 | _updates.add(inserted)
122 | callback.invoke(updateId)
123 | }
124 | }
125 | }
126 | }
127 |
128 | fun update() {
129 | launch {
130 | val request = Request.Builder()
131 | .url(currentUpdate!!.updateUri!!)
132 | .build()
133 |
134 | client.newCall(request).execute().use { response ->
135 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
136 | val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string())
137 | currentUpdate!!.let {
138 | withContext(Dispatchers.Main) {
139 | it.kernelName = update.kernelName
140 | it.kernelVersion = update.kernelVersion
141 | it.kernelLink = update.kernelLink
142 | it.kernelChangelogUrl = update.kernelChangelogUrl
143 | it.kernelDate = update.kernelDate
144 | it.kernelSha1 = update.kernelSha1
145 | it.supportLink = update.supportLink
146 | it.lastUpdated = Date()
147 | viewModelScope.launch(Dispatchers.IO) {
148 | updateDao.update(it)
149 | }
150 | }
151 | }
152 | }
153 | }
154 | }
155 |
156 | fun downloadChangelog(callback: () -> Unit) {
157 | launch {
158 | val request = Request.Builder()
159 | .url(currentUpdate!!.kernelChangelogUrl)
160 | .build()
161 |
162 | client.newCall(request).execute().use { response ->
163 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
164 | changelog = response.body!!.string()
165 | withContext(Dispatchers.Main) {
166 | callback.invoke()
167 | }
168 | }
169 | }
170 | }
171 |
172 | private fun insertDownload(context: Context, filename: String): Uri? {
173 | val resolver = context.contentResolver
174 | val values = ContentValues()
175 | values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
176 | values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
177 | values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
178 | return resolver.insert(MediaStore.Files.getContentUri("external"), values)
179 | }
180 |
181 | fun downloadKernel(context: Context) {
182 | launch {
183 | val remoteUri = Uri.parse(currentUpdate!!.kernelLink)
184 | val filename = Path(remoteUri.path!!).name
185 | val localUri = insertDownload(context, filename)
186 | localUri!!.let { uri ->
187 | val request = Request.Builder()
188 | .url(remoteUri.toString())
189 | .build()
190 |
191 | client.newCall(request).execute().use { response ->
192 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
193 | response.body!!.byteStream().use { inputStream ->
194 | context.contentResolver.openOutputStream(uri)!!.use { outputStream ->
195 | inputStream.copyTo(outputStream)
196 | }
197 | }
198 | log(context, "Saved $filename to Downloads")
199 | }
200 | }
201 | }
202 | }
203 |
204 | fun delete(callback: () -> Unit) {
205 | launch {
206 | updateDao.delete(currentUpdate!!)
207 | withContext(Dispatchers.Main) {
208 | _updates.remove(currentUpdate!!)
209 | callback.invoke()
210 | currentUpdate = null
211 | }
212 | }
213 | }
214 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.common
2 |
3 | import android.content.Context
4 | import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex
5 | import com.github.capntrips.kernelflasher.common.types.partitions.FstabEntry
6 | import com.topjohnwu.superuser.Shell
7 | import com.topjohnwu.superuser.nio.ExtendedFile
8 | import com.topjohnwu.superuser.nio.FileSystemManager
9 | import kotlinx.serialization.json.Json
10 | import java.io.File
11 | import java.security.DigestOutputStream
12 | import java.security.MessageDigest
13 |
14 | object PartitionUtil {
15 | val PartitionNames = listOf(
16 | "boot",
17 | "dtbo",
18 | "init_boot",
19 | "recovery",
20 | "system_dlkm",
21 | "vbmeta",
22 | "vendor_boot",
23 | "vendor_dlkm",
24 | "vendor_kernel_boot"
25 | )
26 |
27 | val AvailablePartitions = mutableListOf()
28 |
29 | private var fileSystemManager: FileSystemManager? = null
30 | private var bootParent: File? = null
31 |
32 | fun init(context: Context, fileSystemManager: FileSystemManager) {
33 | this.fileSystemManager = fileSystemManager
34 | val fstabEntry = findPartitionFstabEntry(context, "boot")
35 | if (fstabEntry != null) {
36 | bootParent = File(fstabEntry.blkDevice).parentFile
37 | }
38 | val activeSlotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0]
39 | for (partitionName in PartitionNames) {
40 | val blockDevice = findPartitionBlockDevice(context, partitionName, activeSlotSuffix)
41 | if (blockDevice != null && blockDevice.exists()) {
42 | AvailablePartitions.add(partitionName)
43 | }
44 | }
45 | }
46 |
47 | private fun findPartitionFstabEntry(context: Context, partitionName: String): FstabEntry? {
48 | val httools = File(context.filesDir, "httools_static")
49 | val result = Shell.cmd("$httools dump $partitionName").exec().out
50 | if (result.isNotEmpty()) {
51 | return Json.decodeFromString(result[0])
52 | }
53 | return null
54 | }
55 |
56 | fun isPartitionLogical(context: Context, partitionName: String): Boolean {
57 | return findPartitionFstabEntry(context, partitionName)?.fsMgrFlags?.logical == true
58 | }
59 |
60 | fun findPartitionBlockDevice(
61 | context: Context,
62 | partitionName: String,
63 | slotSuffix: String
64 | ): ExtendedFile? {
65 | var blockDevice: ExtendedFile? = null
66 | val fstabEntry = findPartitionFstabEntry(context, partitionName)
67 | if (fstabEntry != null) {
68 | if (fstabEntry.fsMgrFlags?.logical == true) {
69 | if (fstabEntry.logicalPartitionName == "$partitionName$slotSuffix") {
70 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice)
71 | }
72 | } else {
73 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice)
74 | if (blockDevice.name != "$partitionName$slotSuffix") {
75 | blockDevice =
76 | fileSystemManager!!.getFile(blockDevice.parentFile, "$partitionName$slotSuffix")
77 | }
78 | }
79 | }
80 | if (blockDevice == null || !blockDevice.exists()) {
81 | val siblingDevice = if (bootParent != null) fileSystemManager!!.getFile(
82 | bootParent!!,
83 | "$partitionName$slotSuffix"
84 | ) else null
85 | val physicalDevice =
86 | fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix")
87 | val logicalDevice = fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix")
88 | if (siblingDevice?.exists() == true) {
89 | blockDevice = physicalDevice
90 | } else if (physicalDevice.exists()) {
91 | blockDevice = physicalDevice
92 | } else if (logicalDevice.exists()) {
93 | blockDevice = logicalDevice
94 | }
95 | }
96 | return blockDevice
97 | }
98 |
99 | @Suppress("unused")
100 | fun partitionAvb(context: Context, partitionName: String): String {
101 | val httools = File(context.filesDir, "httools_static")
102 | val result = Shell.cmd("$httools avb $partitionName").exec().out
103 | return if (result.isNotEmpty()) result[0] else ""
104 | }
105 |
106 | fun flashBlockDevice(
107 | image: ExtendedFile,
108 | blockDevice: ExtendedFile,
109 | hashAlgorithm: String
110 | ): String {
111 | val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt()
112 | val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt()
113 | if (partitionSize < imageSize) {
114 | throw Error("Partition ${blockDevice.name} is smaller than image")
115 | }
116 | if (partitionSize > imageSize) {
117 | Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice").exec()
118 | }
119 | val messageDigest = MessageDigest.getInstance(hashAlgorithm)
120 | image.newInputStream().use { inputStream ->
121 | blockDevice.newOutputStream().use { outputStream ->
122 | DigestOutputStream(outputStream, messageDigest).use { digestOutputStream ->
123 | inputStream.copyTo(digestOutputStream)
124 | }
125 | }
126 | }
127 | return messageDigest.digest().toHex()
128 | }
129 |
130 | @Suppress("SameParameterValue")
131 | fun flashLogicalPartition(
132 | context: Context,
133 | image: ExtendedFile,
134 | blockDevice: ExtendedFile,
135 | partitionName: String,
136 | slotSuffix: String,
137 | hashAlgorithm: String,
138 | addMessage: (message: String) -> Unit
139 | ): String {
140 | val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt()
141 | val lptools = File(context.filesDir, "lptools_static")
142 | Shell.cmd("$lptools remove ${partitionName}_kf").exec()
143 | if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) {
144 | if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) {
145 | if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) {
146 | val temporaryBlockDevice =
147 | fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf")
148 | val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm)
149 | if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix")
150 | .exec().isSuccess
151 | ) {
152 | return hash
153 | } else {
154 | throw Error("Replacing $partitionName$slotSuffix failed")
155 | }
156 | } else {
157 | throw Error("Remapping ${partitionName}_kf failed")
158 | }
159 | } else {
160 | throw Error("Unmapping ${partitionName}_kf failed")
161 | }
162 | } else {
163 | addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...")
164 | val httools = File(context.filesDir, "httools_static")
165 | if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) {
166 | val verityBlockDevice = blockDevice.parentFile!!.getChildFile("${partitionName}-verity")
167 | if (verityBlockDevice.exists()) {
168 | if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) {
169 | throw Error("Unmapping ${partitionName}-verity failed")
170 | }
171 | }
172 | if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) {
173 | if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)")
174 | .exec().isSuccess
175 | ) {
176 | if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) {
177 | val hash = flashBlockDevice(image, blockDevice, hashAlgorithm)
178 | if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) {
179 | return hash
180 | } else {
181 | throw Error("Mounting $partitionName failed")
182 | }
183 | } else {
184 | throw Error("Remapping $partitionName$slotSuffix failed")
185 | }
186 | } else {
187 | throw Error("Resizing $partitionName$slotSuffix failed")
188 | }
189 | } else {
190 | throw Error("Unmapping $partitionName$slotSuffix failed")
191 | }
192 | } else {
193 | throw Error("Unmounting $partitionName failed")
194 | }
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.slot
2 |
3 | import androidx.activity.compose.BackHandler
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.ColumnScope
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.offset
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material.ExperimentalMaterialApi
17 | import androidx.compose.material3.AlertDialog
18 | import androidx.compose.material3.ButtonDefaults
19 | import androidx.compose.material3.Checkbox
20 | import androidx.compose.material3.ExperimentalMaterial3Api
21 | import androidx.compose.material3.MaterialTheme
22 | import androidx.compose.material3.OutlinedButton
23 | import androidx.compose.material3.Text
24 | import androidx.compose.runtime.Composable
25 | import androidx.compose.runtime.derivedStateOf
26 | import androidx.compose.runtime.getValue
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.draw.alpha
31 | import androidx.compose.ui.graphics.Color
32 | import androidx.compose.ui.platform.LocalContext
33 | import androidx.compose.ui.res.stringResource
34 | import androidx.compose.ui.text.font.FontWeight
35 | import androidx.compose.ui.unit.ExperimentalUnitApi
36 | import androidx.compose.ui.unit.dp
37 | import androidx.navigation.NavController
38 | import com.github.capntrips.kernelflasher.R
39 | import com.github.capntrips.kernelflasher.common.PartitionUtil
40 | import com.github.capntrips.kernelflasher.ui.components.DataCard
41 | import com.github.capntrips.kernelflasher.ui.components.DialogButton
42 | import com.github.capntrips.kernelflasher.ui.components.FlashButton
43 | import com.github.capntrips.kernelflasher.ui.components.FlashList
44 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
45 | import com.github.capntrips.kernelflasher.ui.components.SlotCard
46 | import kotlinx.serialization.ExperimentalSerializationApi
47 |
48 | @ExperimentalAnimationApi
49 | @ExperimentalMaterialApi
50 | @ExperimentalMaterial3Api
51 | @ExperimentalUnitApi
52 | @ExperimentalSerializationApi
53 | @Composable
54 | fun ColumnScope.SlotFlashContent(
55 | viewModel: SlotViewModel,
56 | slotSuffix: String,
57 | navController: NavController
58 | ) {
59 | val context = LocalContext.current
60 |
61 | val isRefreshing by remember { derivedStateOf { viewModel.isRefreshing } }
62 | val currentRoute = navController.currentDestination!!.route.orEmpty()
63 |
64 | BackHandler(
65 | enabled = ((currentRoute.endsWith("/flash/ak3") ||
66 | currentRoute.endsWith("/flash/image/flash") ||
67 | currentRoute.endsWith("/backup/backup")) && isRefreshing.value)
68 | ) {
69 |
70 | }
71 |
72 | if (!listOf(
73 | "/flash/ak3",
74 | "/flash/image/flash",
75 | "/backup/backup"
76 | ).any { navController.currentDestination!!.route!!.endsWith(it) }
77 | ) {
78 | SlotCard(
79 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot),
80 | viewModel = viewModel,
81 | navController = navController,
82 | isSlotScreen = true,
83 | showDlkm = false
84 | )
85 | Spacer(Modifier.height(16.dp))
86 | if (navController.currentDestination!!.route!!.endsWith("/flash")) {
87 | DataCard(stringResource(R.string.flash))
88 | Spacer(Modifier.height(5.dp))
89 | FlashButton(stringResource(R.string.flash_ak3_zip), "zip", callback = { uri ->
90 | navController.navigate("slot$slotSuffix/flash/ak3") {
91 | popUpTo("slot$slotSuffix")
92 | }
93 | viewModel.flashAk3(context, uri)
94 | })
95 | FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip", callback = { uri ->
96 | navController.navigate("slot$slotSuffix/flash/ak3") {
97 | popUpTo("slot$slotSuffix")
98 | }
99 | viewModel.flashAk3_mkbootfs(context, uri)
100 | })
101 | FlashButton(stringResource(R.string.flash_ksu_lkm), "ko", callback = { uri ->
102 | navController.navigate("slot$slotSuffix/flash/image/flash") {
103 | popUpTo("slot$slotSuffix")
104 | }
105 | viewModel.flashKsuDriver(context, uri)
106 | })
107 | MyOutlinedButton(
108 | onclick = {
109 | navController.navigate("slot$slotSuffix/flash/image")
110 | }
111 | ) {
112 | Text(stringResource(R.string.flash_partition_image))
113 | }
114 | } else if (navController.currentDestination!!.route!!.endsWith("/flash/image")) {
115 | DataCard(stringResource(R.string.flash_partition_image))
116 | Spacer(Modifier.height(5.dp))
117 | for (partitionName in PartitionUtil.AvailablePartitions) {
118 | FlashButton(partitionName, "img", callback = { uri ->
119 | navController.navigate("slot$slotSuffix/flash/image/flash") {
120 | popUpTo("slot$slotSuffix")
121 | }
122 | viewModel.flashImage(context, uri, partitionName)
123 | })
124 | }
125 | } else if (navController.currentDestination!!.route!!.endsWith("/backup")) {
126 | DataCard(stringResource(R.string.backup))
127 | Spacer(Modifier.height(5.dp))
128 | val disabledColor = ButtonDefaults.buttonColors(
129 | Color.Transparent,
130 | MaterialTheme.colorScheme.onSurface
131 | )
132 | for (partitionName in PartitionUtil.AvailablePartitions) {
133 | OutlinedButton(
134 | modifier = Modifier
135 | .fillMaxWidth()
136 | .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f),
137 | shape = RoundedCornerShape(4.dp),
138 | colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor,
139 | onClick = {
140 | viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!!
141 | },
142 | ) {
143 | Box(Modifier.fillMaxWidth()) {
144 | Checkbox(
145 | viewModel.backupPartitions[partitionName]!!, null,
146 | Modifier
147 | .align(Alignment.CenterStart)
148 | .offset(x = -(16.dp))
149 | )
150 | Text(partitionName, Modifier.align(Alignment.Center))
151 | }
152 | }
153 | }
154 | MyOutlinedButton(
155 | {
156 | viewModel.backup(context)
157 | navController.navigate("slot$slotSuffix/backup/backup") {
158 | popUpTo("slot$slotSuffix")
159 | }
160 | },
161 | enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty()
162 | ) {
163 | Text(stringResource(R.string.backup))
164 | }
165 | }
166 | } else {
167 | Text("")
168 | FlashList(
169 | stringResource(if (navController.currentDestination!!.route!!.endsWith("/backup/backup")) R.string.backup else R.string.flash),
170 | if (navController.currentDestination!!.route!!.contains("ak3")) viewModel.uiPrintedOutput else viewModel.flashOutput
171 | ) {
172 | AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.wasFlashSuccess.value != null) {
173 | Column {
174 | if (navController.currentDestination!!.route!!.contains("ak3")) {
175 | MyOutlinedButton(
176 | { viewModel.saveLog(context) }
177 | ) {
178 | if (navController.currentDestination!!.route!!.contains("ak3")) {
179 | Text(stringResource(R.string.save_ak3_log))
180 | } else if (navController.currentDestination!!.route!!.endsWith("/backup/backup")) {
181 | Text(stringResource(R.string.save_backup_log))
182 | } else {
183 | Text(stringResource(R.string.save_flash_log))
184 | }
185 | }
186 | }
187 | if (navController.currentDestination!!.route!!.contains("ak3")) {
188 | AnimatedVisibility(!navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/flash/ak3") && viewModel.wasFlashSuccess.value != false) {
189 | MyOutlinedButton(
190 | {
191 | viewModel.backupZip(context) {
192 | navController.navigate("slot$slotSuffix/backups") {
193 | popUpTo("slot$slotSuffix")
194 | }
195 | }
196 | }
197 | ) {
198 | Text(stringResource(R.string.save_ak3_zip_as_backup))
199 | }
200 | }
201 | }
202 | if (viewModel.wasFlashSuccess.value == true && viewModel.showCautionDialog == true) {
203 | AlertDialog(
204 | onDismissRequest = { viewModel.hideCautionDialog() },
205 | title = {
206 | Text(
207 | "CAUTION!",
208 | style = MaterialTheme.typography.titleLarge,
209 | fontWeight = FontWeight.Bold
210 | )
211 | },
212 | text = {
213 | Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
214 | Text("You have flashed to inactive slot!", fontWeight = FontWeight.Bold)
215 | Text(
216 | "But the active slot is not changed after flashing.",
217 | fontWeight = FontWeight.Bold
218 | )
219 | Text(
220 | "Change active slot or return to System Updater to complete OTA.",
221 | fontWeight = FontWeight.Bold
222 | )
223 | Text(
224 | "Do not reboot from here, unless you know what you are doing.",
225 | fontWeight = FontWeight.Bold
226 | )
227 | }
228 | },
229 | confirmButton = {
230 | DialogButton(
231 | "CHANGE SLOT"
232 | ) {
233 | viewModel.hideCautionDialog()
234 | viewModel.switchSlot(context)
235 | }
236 | },
237 | dismissButton = {
238 | DialogButton(
239 | "CANCEL"
240 | ) {
241 | viewModel.hideCautionDialog()
242 | }
243 | },
244 | modifier = Modifier.padding(16.dp)
245 | )
246 | }
247 | if (viewModel.wasFlashSuccess.value != false && navController.currentDestination!!.route!!.endsWith(
248 | "/backup/backup"
249 | )
250 | ) {
251 | MyOutlinedButton(
252 | { navController.popBackStack() }
253 | ) {
254 | Text(stringResource(R.string.back))
255 | }
256 | } else {
257 | MyOutlinedButton(
258 | { navController.navigate("reboot") }
259 | ) {
260 | Text(stringResource(R.string.reboot))
261 | }
262 | }
263 | }
264 | }
265 | }
266 | }
267 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.backups
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.ColumnScope
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.offset
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material3.ButtonDefaults
13 | import androidx.compose.material3.Checkbox
14 | import androidx.compose.material3.ExperimentalMaterial3Api
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.OutlinedButton
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.mutableIntStateOf
20 | import androidx.compose.runtime.remember
21 | import androidx.compose.ui.Alignment
22 | import androidx.compose.ui.Modifier
23 | import androidx.compose.ui.draw.alpha
24 | import androidx.compose.ui.graphics.Color
25 | import androidx.compose.ui.platform.LocalContext
26 | import androidx.compose.ui.res.stringResource
27 | import androidx.compose.ui.text.font.FontFamily
28 | import androidx.compose.ui.text.font.FontStyle
29 | import androidx.compose.ui.text.font.FontWeight
30 | import androidx.compose.ui.text.style.TextAlign
31 | import androidx.compose.ui.unit.ExperimentalUnitApi
32 | import androidx.compose.ui.unit.dp
33 | import androidx.navigation.NavController
34 | import com.github.capntrips.kernelflasher.R
35 | import com.github.capntrips.kernelflasher.common.PartitionUtil
36 | import com.github.capntrips.kernelflasher.ui.components.DataCard
37 | import com.github.capntrips.kernelflasher.ui.components.DataRow
38 | import com.github.capntrips.kernelflasher.ui.components.DataSet
39 | import com.github.capntrips.kernelflasher.ui.components.FlashList
40 | import com.github.capntrips.kernelflasher.ui.components.MyOutlinedButton
41 | import com.github.capntrips.kernelflasher.ui.components.SlotCard
42 | import com.github.capntrips.kernelflasher.ui.components.ViewButton
43 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel
44 |
45 | @ExperimentalMaterial3Api
46 | @ExperimentalUnitApi
47 | @Composable
48 | fun ColumnScope.SlotBackupsContent(
49 | slotViewModel: SlotViewModel,
50 | backupsViewModel: BackupsViewModel,
51 | slotSuffix: String,
52 | navController: NavController
53 | ) {
54 | val context = LocalContext.current
55 | if (!navController.currentDestination!!.route!!.contains("/backups/{backupId}/restore")) {
56 | SlotCard(
57 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else if (slotSuffix == "_b") R.string.slot_b else R.string.slot),
58 | viewModel = slotViewModel,
59 | navController = navController,
60 | isSlotScreen = true,
61 | showDlkm = false,
62 | )
63 | Spacer(Modifier.height(16.dp))
64 | if (backupsViewModel.currentBackup != null && backupsViewModel.backups.containsKey(
65 | backupsViewModel.currentBackup
66 | )
67 | ) {
68 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!)
69 | DataCard(backupsViewModel.currentBackup!!) {
70 | val cardWidth = remember { mutableIntStateOf(0) }
71 | DataRow(
72 | stringResource(R.string.backup_type),
73 | currentBackup.type,
74 | mutableMaxWidth = cardWidth
75 | )
76 | DataRow(
77 | stringResource(R.string.kernel_version),
78 | currentBackup.kernelVersion,
79 | mutableMaxWidth = cardWidth,
80 | clickable = true
81 | )
82 | if (currentBackup.type == "raw") {
83 | if (!currentBackup.bootSha1.isNullOrEmpty()) {
84 | DataRow(
85 | label = stringResource(R.string.boot_sha1),
86 | value = currentBackup.bootSha1.substring(0, 8),
87 | valueStyle = MaterialTheme.typography.titleSmall.copy(
88 | fontFamily = FontFamily.Monospace,
89 | fontWeight = FontWeight.Medium
90 | ),
91 | mutableMaxWidth = cardWidth
92 | )
93 | }
94 | if (currentBackup.hashes != null) {
95 | val hashWidth = remember { mutableIntStateOf(0) }
96 | DataSet(stringResource(R.string.hashes)) {
97 | for (partitionName in PartitionUtil.PartitionNames) {
98 | val hash = currentBackup.hashes.get(partitionName)
99 | if (hash != null) {
100 | DataRow(
101 | label = partitionName,
102 | value = hash.takeIf { it.isNotEmpty() }?.substring(0, 8) ?: "Hash not found!",
103 | valueStyle = MaterialTheme.typography.titleSmall.copy(
104 | fontFamily = FontFamily.Monospace,
105 | fontWeight = FontWeight.Medium
106 | ),
107 | mutableMaxWidth = hashWidth
108 | )
109 | }
110 | }
111 | }
112 | }
113 | }
114 | }
115 | AnimatedVisibility(!slotViewModel.isRefreshing.value) {
116 | Column {
117 | Spacer(Modifier.height(5.dp))
118 | if (slotViewModel.isActive) {
119 | if (currentBackup.type == "raw") {
120 | MyOutlinedButton(
121 | {
122 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore")
123 | }
124 | ) {
125 | Text(stringResource(R.string.restore))
126 | }
127 | } else if (currentBackup.type == "ak3") {
128 | MyOutlinedButton(
129 | {
130 | slotViewModel.flashAk3(
131 | context,
132 | backupsViewModel.currentBackup!!,
133 | currentBackup.filename!!
134 | )
135 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") {
136 | popUpTo("slot$slotSuffix")
137 | }
138 | }
139 | ) {
140 | Text(stringResource(R.string.flash))
141 | }
142 | MyOutlinedButton(
143 | {
144 | slotViewModel.flashAk3_mkbootfs(
145 | context,
146 | backupsViewModel.currentBackup!!,
147 | currentBackup.filename!!
148 | )
149 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") {
150 | popUpTo("slot$slotSuffix")
151 | }
152 | }
153 | ) {
154 | Text(stringResource(R.string.flash_ak3_zip_mkbootfs))
155 | }
156 | }
157 | }
158 | MyOutlinedButton(
159 | { backupsViewModel.delete(context) { navController.popBackStack() } }
160 | ) {
161 | Text(stringResource(R.string.delete))
162 | }
163 | }
164 | }
165 | } else {
166 | DataCard(stringResource(R.string.backups))
167 | val backups = backupsViewModel.backups.filter {
168 | it.value.bootSha1.isNullOrEmpty() || it.value.bootSha1.equals(slotViewModel.sha1) || it.value.type == "ak3"
169 | }
170 | if (backups.isNotEmpty()) {
171 | for (id in backups.keys.sortedByDescending { it }) {
172 | Spacer(Modifier.height(16.dp))
173 | DataCard(
174 | title = id,
175 | button = {
176 | AnimatedVisibility(!slotViewModel.isRefreshing.value) {
177 | ViewButton(onClick = {
178 | navController.navigate("slot$slotSuffix/backups/$id")
179 | })
180 | }
181 | }
182 | ) {
183 | DataRow(
184 | stringResource(R.string.kernel_version),
185 | backups[id]!!.kernelVersion,
186 | clickable = true
187 | )
188 | }
189 | }
190 | } else {
191 | Spacer(Modifier.height(32.dp))
192 | Text(
193 | stringResource(R.string.no_backups_found),
194 | modifier = Modifier.fillMaxWidth(),
195 | textAlign = TextAlign.Center,
196 | fontStyle = FontStyle.Italic
197 | )
198 | }
199 | }
200 | } else if (navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/restore")) {
201 | DataCard(stringResource(R.string.restore))
202 | Spacer(Modifier.height(5.dp))
203 | val disabledColor = ButtonDefaults.buttonColors(
204 | Color.Transparent,
205 | MaterialTheme.colorScheme.onSurface
206 | )
207 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!)
208 | if (currentBackup.hashes != null) {
209 | for (partitionName in PartitionUtil.PartitionNames) {
210 | val hash = currentBackup.hashes.get(partitionName)
211 | if (hash != null) {
212 | OutlinedButton(
213 | modifier = Modifier
214 | .fillMaxWidth()
215 | .alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f),
216 | shape = RoundedCornerShape(4.dp),
217 | colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor,
218 | enabled = backupsViewModel.backupPartitions[partitionName] != null,
219 | onClick = {
220 | backupsViewModel.backupPartitions[partitionName] =
221 | !backupsViewModel.backupPartitions[partitionName]!!
222 | },
223 | ) {
224 | Box(Modifier.fillMaxWidth()) {
225 | Checkbox(
226 | backupsViewModel.backupPartitions[partitionName] == true, null,
227 | Modifier
228 | .align(Alignment.CenterStart)
229 | .offset(x = -(16.dp))
230 | )
231 | Text(partitionName, Modifier.align(Alignment.Center))
232 | }
233 | }
234 | }
235 | }
236 | } else {
237 | Text(
238 | stringResource(R.string.partition_selection_unavailable),
239 | modifier = Modifier.fillMaxWidth(),
240 | textAlign = TextAlign.Center,
241 | fontStyle = FontStyle.Italic
242 | )
243 | Spacer(Modifier.height(5.dp))
244 | }
245 | MyOutlinedButton(
246 | {
247 | backupsViewModel.restore(context, slotSuffix)
248 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") {
249 | popUpTo("slot$slotSuffix")
250 | }
251 | },
252 | enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none {
253 | currentBackup.hashes.get(
254 | it
255 | ) != null && backupsViewModel.backupPartitions[it] == null
256 | } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty())
257 | ) {
258 | Text(stringResource(R.string.restore))
259 | }
260 | } else {
261 | FlashList(
262 | stringResource(R.string.restore),
263 | backupsViewModel.restoreOutput
264 | ) {
265 | AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) {
266 | Column {
267 | if (backupsViewModel.wasRestored != false) {
268 | MyOutlinedButton(
269 | { navController.navigate("reboot") }
270 | ) {
271 | Text(stringResource(R.string.reboot))
272 | }
273 | }
274 | }
275 | }
276 | }
277 | }
278 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.capntrips.kernelflasher.ui.screens.backups
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.util.Log
6 | import android.widget.Toast
7 | import androidx.compose.runtime.MutableState
8 | import androidx.compose.runtime.mutableStateListOf
9 | import androidx.compose.runtime.mutableStateMapOf
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.snapshots.SnapshotStateList
12 | import androidx.compose.runtime.snapshots.SnapshotStateMap
13 | import androidx.lifecycle.ViewModel
14 | import androidx.lifecycle.viewModelScope
15 | import androidx.navigation.NavController
16 | import com.github.capntrips.kernelflasher.common.PartitionUtil
17 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream
18 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.readText
19 | import com.github.capntrips.kernelflasher.common.types.backups.Backup
20 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions
21 | import com.topjohnwu.superuser.Shell
22 | import com.topjohnwu.superuser.nio.ExtendedFile
23 | import com.topjohnwu.superuser.nio.FileSystemManager
24 | import kotlinx.coroutines.Dispatchers
25 | import kotlinx.coroutines.launch
26 | import kotlinx.coroutines.withContext
27 | import kotlinx.serialization.json.Json
28 | import java.io.File
29 | import java.io.FileInputStream
30 | import java.time.LocalDateTime
31 | import java.time.format.DateTimeFormatter
32 | import java.util.Properties
33 |
34 | class BackupsViewModel(
35 | context: Context,
36 | private val fileSystemManager: FileSystemManager,
37 | private val navController: NavController,
38 | private val _isRefreshing: MutableState,
39 | private val _backups: MutableMap
40 | ) : ViewModel() {
41 | companion object {
42 | const val TAG: String = "KernelFlasher/BackupsState"
43 | }
44 |
45 | private val _restoreOutput: SnapshotStateList = mutableStateListOf()
46 | var currentBackup: String? = null
47 | set(value) {
48 | if (value != field) {
49 | if (_backups[value]?.hashes != null) {
50 | PartitionUtil.AvailablePartitions.forEach { partitionName ->
51 | if (_backups[value]!!.hashes!!.get(partitionName) != null) {
52 | _backupPartitions[partitionName] = true
53 | }
54 | }
55 | }
56 | field = value
57 | }
58 | }
59 | var wasRestored: Boolean? = null
60 | private val _backupPartitions: SnapshotStateMap = mutableStateMapOf()
61 | private val hashAlgorithm: String = "SHA-256"
62 |
63 | @Deprecated(
64 | "Backup migration will be removed in the first stable release",
65 | level = DeprecationLevel.WARNING
66 | )
67 | private var _needsMigration: MutableState = mutableStateOf(false)
68 |
69 | val restoreOutput: List
70 | get() = _restoreOutput
71 | val backupPartitions: MutableMap
72 | get() = _backupPartitions
73 | val isRefreshing: Boolean
74 | get() = _isRefreshing.value
75 | val backups: Map
76 | get() = _backups
77 |
78 | init {
79 | refresh(context)
80 | }
81 |
82 | fun refresh(context: Context) {
83 | val oldDir = context.getExternalFilesDir(null)
84 | val oldBackupsDir = File(oldDir, "backups")
85 | // Deprecated: Backup migration will be removed in the first stable release
86 | _needsMigration.value = oldBackupsDir.exists() && oldBackupsDir.listFiles()?.size!! > 0
87 | @SuppressLint("SdCardPath")
88 | val externalDir = File("/sdcard/KernelFlasher")
89 | val backupsDir = fileSystemManager.getFile("$externalDir/backups")
90 | if (backupsDir.exists()) {
91 | val children = backupsDir.listFiles()
92 | if (children != null) {
93 | for (child in children.sortedByDescending { it.name }) {
94 | if (!child.isDirectory) {
95 | continue
96 | }
97 | val jsonFile = child.getChildFile("backup.json")
98 | if (jsonFile.exists()) {
99 | _backups[child.name] = Json.decodeFromString(jsonFile.readText())
100 | }
101 | }
102 | }
103 | }
104 | }
105 |
106 | private fun launch(block: suspend () -> Unit) {
107 | viewModelScope.launch(Dispatchers.IO) {
108 | _isRefreshing.value = true
109 | try {
110 | block()
111 | } catch (e: Exception) {
112 | withContext(Dispatchers.Main) {
113 | Log.e(TAG, e.message, e)
114 | navController.navigate("error/${e.message}") {
115 | popUpTo("main")
116 | }
117 | }
118 | }
119 | _isRefreshing.value = false
120 | }
121 | }
122 |
123 | @Suppress("SameParameterValue")
124 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
125 | Log.d(TAG, message)
126 | if (!shouldThrow) {
127 | viewModelScope.launch(Dispatchers.Main) {
128 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
129 | }
130 | } else {
131 | throw Exception(message)
132 | }
133 | }
134 |
135 | fun clearCurrent() {
136 | currentBackup = null
137 | clearRestore()
138 | }
139 |
140 | private fun addMessage(message: String) {
141 | viewModelScope.launch(Dispatchers.Main) {
142 | _restoreOutput.add(message)
143 | }
144 | }
145 |
146 | @Suppress("FunctionName")
147 | private fun _clearRestore() {
148 | _restoreOutput.clear()
149 | wasRestored = null
150 | }
151 |
152 | private fun clearRestore() {
153 | _clearRestore()
154 | _backupPartitions.clear()
155 | }
156 |
157 | @Suppress("unused")
158 | @SuppressLint("SdCardPath")
159 | fun saveLog(context: Context) {
160 | launch {
161 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
162 | val log = File("/sdcard/Download/restore-log--$now.log")
163 | log.writeText(restoreOutput.joinToString("\n"))
164 | if (log.exists()) {
165 | log(context, "Saved restore log to $log")
166 | } else {
167 | log(context, "Failed to save $log", shouldThrow = true)
168 | }
169 | }
170 | }
171 |
172 | private fun restorePartitions(
173 | context: Context,
174 | source: ExtendedFile,
175 | slotSuffix: String
176 | ): Partitions? {
177 | val partitions = HashMap()
178 | for (partitionName in PartitionUtil.PartitionNames) {
179 | if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) {
180 | val image = source.getChildFile("$partitionName.img")
181 | if (image.exists()) {
182 | val blockDevice =
183 | PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix)
184 | if (blockDevice != null && blockDevice.exists()) {
185 | addMessage("Restoring $partitionName")
186 | partitions[partitionName] =
187 | if (PartitionUtil.isPartitionLogical(context, partitionName)) {
188 | PartitionUtil.flashLogicalPartition(
189 | context,
190 | image,
191 | blockDevice,
192 | partitionName,
193 | slotSuffix,
194 | hashAlgorithm
195 | ) { message ->
196 | addMessage(message)
197 | }
198 | } else {
199 | PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm)
200 | }
201 | } else {
202 | log(context, "Partition $partitionName was not found", shouldThrow = true)
203 | }
204 | }
205 | }
206 | }
207 | if (partitions.isNotEmpty()) {
208 | return Partitions.from(partitions)
209 | }
210 | return null
211 | }
212 |
213 | fun restore(context: Context, slotSuffix: String) {
214 | launch {
215 | _clearRestore()
216 | @SuppressLint("SdCardPath")
217 | val externalDir = File("/sdcard/KernelFlasher")
218 | val backupsDir = fileSystemManager.getFile("$externalDir/backups")
219 | val backupDir = backupsDir.getChildFile(currentBackup!!)
220 | if (!backupDir.exists()) {
221 | log(context, "Backup $currentBackup does not exists", shouldThrow = true)
222 | return@launch
223 | }
224 | addMessage("Restoring backup $currentBackup")
225 | val hashes = restorePartitions(context, backupDir, slotSuffix)
226 | if (hashes == null) {
227 | log(context, "No partitions restored", shouldThrow = true)
228 | }
229 | addMessage("Backup $currentBackup restored")
230 | wasRestored = true
231 | }
232 | }
233 |
234 | fun delete(context: Context, callback: () -> Unit) {
235 | launch {
236 | @SuppressLint("SdCardPath")
237 | val externalDir = File("/sdcard/KernelFlasher")
238 | val backupsDir = fileSystemManager.getFile("$externalDir/backups")
239 | val backupDir = backupsDir.getChildFile(currentBackup!!)
240 | if (!backupDir.exists()) {
241 | log(context, "Backup $currentBackup does not exists", shouldThrow = true)
242 | return@launch
243 | }
244 | backupDir.deleteRecursively()
245 | _backups.remove(currentBackup!!)
246 | withContext(Dispatchers.Main) {
247 | callback.invoke()
248 | }
249 | }
250 | }
251 |
252 | @SuppressLint("SdCardPath")
253 | @Deprecated("Backup migration will be removed in the first stable release")
254 | fun migrate(context: Context) {
255 | launch {
256 | val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher")
257 | if (!externalDir.exists()) {
258 | if (!externalDir.mkdir()) {
259 | log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true)
260 | }
261 | }
262 | val backupsDir = externalDir.getChildFile("backups")
263 | if (!backupsDir.exists()) {
264 | if (!backupsDir.mkdir()) {
265 | log(context, "Failed to create backups dir", shouldThrow = true)
266 | }
267 | }
268 | val oldDir = context.getExternalFilesDir(null)
269 | val oldBackupsDir = File(oldDir, "backups")
270 | if (oldBackupsDir.exists()) {
271 | val indentedJson = Json { prettyPrint = true }
272 | val children = oldBackupsDir.listFiles()
273 | if (children != null) {
274 | for (child in children.sortedByDescending { it.name }) {
275 | if (!child.isDirectory) {
276 | child.delete()
277 | continue
278 | }
279 | val propFile = File(child, "backup.prop")
280 |
281 | @Suppress("BlockingMethodInNonBlockingContext")
282 | val inputStream = FileInputStream(propFile)
283 | val props = Properties()
284 | @Suppress("BlockingMethodInNonBlockingContext")
285 | props.load(inputStream)
286 |
287 | val name = child.name
288 | val type = props.getProperty("type", "raw")
289 | val kernelVersion = props.getProperty("kernel")
290 | val bootSha1 = if (type == "raw") props.getProperty("sha1") else null
291 | val filename = if (type == "ak3") "ak3.zip" else null
292 | propFile.delete()
293 |
294 | val dest = backupsDir.getChildFile(child.name)
295 | Shell.cmd("mv $child $dest").exec()
296 | if (!dest.exists()) {
297 | throw Error("Too slow")
298 | }
299 | val jsonFile = dest.getChildFile("backup.json")
300 | val backup = Backup(name, type, kernelVersion, bootSha1, filename)
301 | jsonFile.outputStream()
302 | .use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) }
303 | _backups[name] = backup
304 | }
305 | }
306 | oldBackupsDir.delete()
307 | }
308 | refresh(context)
309 | }
310 | }
311 | }
--------------------------------------------------------------------------------