├── app
├── .gitignore
├── src
│ └── main
│ │ ├── 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-rCN
│ │ │ └── strings.xml
│ │ └── values-zh-rTW
│ │ │ └── strings.xml
│ │ ├── aidl
│ │ └── com
│ │ │ └── github
│ │ │ └── rimuruchan
│ │ │ └── kernelflasher
│ │ │ └── IFilesystemService.aidl
│ │ ├── java
│ │ └── com
│ │ │ └── github
│ │ │ └── rimuruchan
│ │ │ └── kernelflasher
│ │ │ ├── ui
│ │ │ ├── theme
│ │ │ │ ├── Color.kt
│ │ │ │ ├── Type.kt
│ │ │ │ └── Theme.kt
│ │ │ ├── screens
│ │ │ │ ├── updates
│ │ │ │ │ ├── UpdatesUrlState.kt
│ │ │ │ │ ├── UpdatesChangelogContent.kt
│ │ │ │ │ ├── UpdatesAddContent.kt
│ │ │ │ │ ├── UpdatesContent.kt
│ │ │ │ │ ├── UpdatesViewContent.kt
│ │ │ │ │ └── UpdatesViewModel.kt
│ │ │ │ ├── error
│ │ │ │ │ └── ErrorScreen.kt
│ │ │ │ ├── reboot
│ │ │ │ │ ├── RebootViewModel.kt
│ │ │ │ │ └── RebootContent.kt
│ │ │ │ ├── main
│ │ │ │ │ ├── MainContent.kt
│ │ │ │ │ └── MainViewModel.kt
│ │ │ │ ├── RefreshableScreen.kt
│ │ │ │ ├── slot
│ │ │ │ │ ├── SlotContent.kt
│ │ │ │ │ └── SlotFlashContent.kt
│ │ │ │ └── backups
│ │ │ │ │ ├── BackupsContent.kt
│ │ │ │ │ ├── SlotBackupsContent.kt
│ │ │ │ │ └── BackupsViewModel.kt
│ │ │ └── components
│ │ │ │ ├── DataSet.kt
│ │ │ │ ├── ViewButton.kt
│ │ │ │ ├── Card.kt
│ │ │ │ ├── DataCard.kt
│ │ │ │ ├── FlashButton.kt
│ │ │ │ ├── DataRow.kt
│ │ │ │ ├── SlotCard.kt
│ │ │ │ └── FlashList.kt
│ │ │ ├── MainListener.kt
│ │ │ ├── common
│ │ │ ├── extensions
│ │ │ │ ├── ByteArray.kt
│ │ │ │ └── ExtendedFile.kt
│ │ │ ├── types
│ │ │ │ ├── partitions
│ │ │ │ │ ├── FsMgrFlags.kt
│ │ │ │ │ ├── FstabEntry.kt
│ │ │ │ │ └── Partitions.kt
│ │ │ │ ├── room
│ │ │ │ │ ├── Converters.kt
│ │ │ │ │ ├── AppDatabase.kt
│ │ │ │ │ └── updates
│ │ │ │ │ │ ├── UpdateDao.kt
│ │ │ │ │ │ └── Update.kt
│ │ │ │ └── backups
│ │ │ │ │ └── Backup.kt
│ │ │ └── PartitionUtil.kt
│ │ │ ├── FilesystemService.kt
│ │ │ └── MainActivity.kt
│ │ ├── assets
│ │ └── flash_ak3.sh
│ │ └── AndroidManifest.xml
└── build.gradle
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── libs.versions.toml
├── .gitignore
├── settings.gradle
├── README.md
├── gradle.properties
├── LICENSE
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/KernelFlasher/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/KernelFlasher/HEAD/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/KernelFlasher/HEAD/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/libhttools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/KernelFlasher/HEAD/app/src/main/jniLibs/arm64-v8a/libhttools_static.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/arm64-v8a/liblptools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/KernelFlasher/HEAD/app/src/main/jniLibs/arm64-v8a/liblptools_static.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/libhttools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/KernelFlasher/HEAD/app/src/main/jniLibs/armeabi-v7a/libhttools_static.so
--------------------------------------------------------------------------------
/app/src/main/jniLibs/armeabi-v7a/liblptools_static.so:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RimuruChan/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/rimuruchan/kernelflasher/IFilesystemService.aidl:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher;
2 |
3 | interface IFilesystemService {
4 | IBinder getFileSystemService();
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Orange500 = Color(0xFFFF9800)
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 |
5 | val Typography = Typography().copy()
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/updates/UpdatesUrlState.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.screens.updates
2 |
3 | @Suppress("unused")
4 | class UpdatesUrlState {
5 | // TODO: validate the url field
6 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/MainListener.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher
2 |
3 | internal class MainListener(private val callback: () -> Unit) {
4 | fun resume() {
5 | callback.invoke()
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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-8.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/extensions/ByteArray.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/java/com/github/rimuruchan/kernelflasher/common/types/partitions/FsMgrFlags.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven { url 'https://jitpack.io' }
14 | }
15 | }
16 | rootProject.name = "Kernel Flasher"
17 | include ':app'
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/types/room/Converters.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/types/partitions/FstabEntry.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Kernel Flasher
2 |
3 | _Forked from [capntrips](https://github.com/capntrips/) and [weiishu](https://github.com/tiann)_
4 |
5 | Kernel Flasher is an Android app to flash, backup, and restore kernels.
6 |
7 | This fork version is fixed for KernelSu user and those who failed to flash after ota.
8 |
9 | ## Usage
10 |
11 | `View` a slot and choose to `Flash` an AK3 zip, `Backup` the kernel related partitions, or `Restore`
12 | a previous backup.
13 |
14 | There are also options to toggle the mount and map status of `vendor_dlkm` and to save `dmesg`
15 | and `logcat`.
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/types/backups/Backup.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.common.types.backups
2 |
3 | import com.github.rimuruchan.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/rimuruchan/kernelflasher/common/types/room/AppDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.common.types.room
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import com.github.rimuruchan.kernelflasher.common.types.room.updates.Update
7 | import com.github.rimuruchan.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/rimuruchan/kernelflasher/FilesystemService.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/types/room/updates/UpdateDao.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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 |
4 |
8 |
9 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/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/rimuruchan/kernelflasher/ui/components/DataSet.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/java/com/github/rimuruchan/kernelflasher/common/extensions/ExtendedFile.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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 |
4 |
5 |
6 |
7 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.theme
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.material3.darkColorScheme
7 | import androidx.compose.material3.dynamicDarkColorScheme
8 | import androidx.compose.material3.dynamicLightColorScheme
9 | import androidx.compose.material3.lightColorScheme
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.platform.LocalContext
12 |
13 | @Composable
14 | fun KernelFlasherTheme(
15 | darkTheme: Boolean = isSystemInDarkTheme(),
16 | dynamicColor: Boolean = true,
17 | content: @Composable () -> Unit
18 | ) {
19 | val colorScheme = when {
20 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
21 | val context = LocalContext.current
22 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
23 | }
24 |
25 | darkTheme -> darkColorScheme()
26 | else -> lightColorScheme()
27 | }
28 | MaterialTheme(
29 | colorScheme = colorScheme,
30 | typography = Typography,
31 | content = content
32 | )
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/components/ViewButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.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 |
5 |
6 |
7 |
8 |
16 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/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/rimuruchan/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.kernelflasher.ui.components.DataCard
18 |
19 | @Suppress("UnusedReceiverParameter")
20 | @ExperimentalMaterial3Api
21 | @ExperimentalUnitApi
22 | @Composable
23 | fun ColumnScope.UpdatesChangelogContent(
24 | viewModel: UpdatesViewModel,
25 | @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController
26 | ) {
27 | viewModel.currentUpdate?.let { currentUpdate ->
28 | DataCard(currentUpdate.kernelName)
29 | Spacer(Modifier.height(16.dp))
30 | Text(
31 | viewModel.changelog!!,
32 | style = LocalTextStyle.current.copy(
33 | fontFamily = FontFamily.Monospace,
34 | fontSize = TextUnit(12.0f, TextUnitType.Sp),
35 | lineHeight = TextUnit(18.0f, TextUnitType.Sp)
36 | )
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/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/rimuruchan/kernelflasher/ui/components/Card.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.BorderStroke
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.Surface
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.graphics.Shape
15 | import androidx.compose.ui.unit.Dp
16 | import androidx.compose.ui.unit.dp
17 |
18 | // TODO: Remove when card is supported in material3: https://m3.material.io/components/cards/implementation/android
19 | @Composable
20 | fun Card(
21 | shape: Shape = RoundedCornerShape(4.dp),
22 | backgroundColor: Color = MaterialTheme.colorScheme.surface,
23 | contentColor: Color = MaterialTheme.colorScheme.onSurface,
24 | border: BorderStroke? = null,
25 | tonalElevation: Dp = 2.dp,
26 | shadowElevation: Dp = 1.dp,
27 | content: @Composable ColumnScope.() -> Unit
28 | ) {
29 | Surface(
30 | shape = shape,
31 | color = backgroundColor,
32 | contentColor = contentColor,
33 | tonalElevation = tonalElevation,
34 | shadowElevation = shadowElevation,
35 | border = border
36 | ) {
37 | Column(
38 | modifier = Modifier
39 | .fillMaxWidth()
40 | .padding(18.dp, (13.788).dp, 18.dp, 18.dp),
41 | content = content
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/components/DataCard.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.ColumnScope
5 | import androidx.compose.foundation.layout.Row
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.foundation.layout.padding
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.unit.dp
16 |
17 | @Composable
18 | fun DataCard(
19 | title: String,
20 | button: @Composable (() -> Unit)? = null,
21 | content: @Composable (ColumnScope.() -> Unit)? = null
22 | ) {
23 | Card {
24 | Row(
25 | modifier = Modifier
26 | .fillMaxWidth()
27 | .padding(0.dp),
28 | horizontalArrangement = Arrangement.SpaceBetween,
29 | verticalAlignment = Alignment.CenterVertically
30 | ) {
31 | Text(
32 | modifier = Modifier
33 | .padding(0.dp, 9.dp, 8.dp, 9.dp)
34 | .weight(1.0f),
35 | text = title,
36 | color = MaterialTheme.colorScheme.primary,
37 | style = MaterialTheme.typography.titleLarge
38 | )
39 | if (button != null) {
40 | button()
41 | }
42 | }
43 | if (content != null) {
44 | Spacer(Modifier.height(10.dp))
45 | content()
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/types/partitions/Partitions.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/rimuruchan/kernelflasher/ui/screens/error/ErrorScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.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/rimuruchan/kernelflasher/ui/screens/updates/UpdatesAddContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.OutlinedButton
10 | import androidx.compose.material3.OutlinedTextField
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.unit.dp
21 | import androidx.navigation.NavController
22 | import com.github.rimuruchan.kernelflasher.R
23 | import kotlinx.serialization.ExperimentalSerializationApi
24 |
25 | @Suppress("UnusedReceiverParameter")
26 | @ExperimentalMaterial3Api
27 | @ExperimentalSerializationApi
28 | @Composable
29 | fun ColumnScope.UpdatesAddContent(
30 | viewModel: UpdatesViewModel,
31 | navController: NavController
32 | ) {
33 | @Suppress("UNUSED_VARIABLE") val context = LocalContext.current
34 | var url by remember { mutableStateOf("") }
35 | OutlinedTextField(
36 | value = url,
37 | onValueChange = { url = it },
38 | label = { Text(stringResource(R.string.url)) },
39 | modifier = Modifier
40 | .fillMaxWidth()
41 | )
42 | Spacer(Modifier.height(5.dp))
43 | OutlinedButton(
44 | modifier = Modifier
45 | .fillMaxWidth(),
46 | shape = RoundedCornerShape(4.dp),
47 | onClick = { viewModel.add(url) { navController.navigate("updates/view/$it") { popUpTo("updates") } } }
48 | ) {
49 | Text(stringResource(R.string.add))
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/components/FlashButton.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.components
2 |
3 | import android.net.Uri
4 | import androidx.activity.compose.rememberLauncherForActivityResult
5 | import androidx.activity.result.contract.ActivityResultContracts
6 | import androidx.compose.animation.ExperimentalAnimationApi
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.shape.RoundedCornerShape
9 | import androidx.compose.material.ExperimentalMaterialApi
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.OutlinedButton
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.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.unit.ExperimentalUnitApi
19 | import androidx.compose.ui.unit.dp
20 | import com.github.rimuruchan.kernelflasher.MainActivity
21 |
22 | @ExperimentalAnimationApi
23 | @ExperimentalMaterialApi
24 | @ExperimentalMaterial3Api
25 | @ExperimentalUnitApi
26 | @Composable
27 | fun FlashButton(
28 | buttonText: String,
29 | callback: (uri: Uri) -> Unit
30 | ) {
31 | val mainActivity = LocalContext.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 | OutlinedButton(
40 | modifier = Modifier
41 | .fillMaxWidth(),
42 | shape = RoundedCornerShape(4.dp),
43 | onClick = {
44 | mainActivity.isAwaitingResult = true
45 | launcher.launch("*/*")
46 | }
47 | ) {
48 | Text(buttonText)
49 | }
50 | result.value?.let { uri ->
51 | if (mainActivity.isAwaitingResult) {
52 | callback.invoke(uri)
53 | }
54 | mainActivity.isAwaitingResult = false
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/reboot/RebootViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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 | }
81 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/reboot/RebootContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.screens.reboot
2 |
3 | import android.os.Build
4 | import android.os.PowerManager
5 | import androidx.compose.foundation.layout.ColumnScope
6 | import androidx.compose.foundation.layout.fillMaxWidth
7 | import androidx.compose.foundation.shape.RoundedCornerShape
8 | import androidx.compose.material3.OutlinedButton
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.platform.LocalContext
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavController
16 | import com.github.rimuruchan.kernelflasher.R
17 |
18 | @Suppress("UnusedReceiverParameter")
19 | @Composable
20 | fun ColumnScope.RebootContent(
21 | viewModel: RebootViewModel,
22 | @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController
23 | ) {
24 | val context = LocalContext.current
25 | OutlinedButton(
26 | modifier = Modifier
27 | .fillMaxWidth(),
28 | shape = RoundedCornerShape(4.dp),
29 | onClick = { viewModel.rebootSystem() }
30 | ) {
31 | Text(stringResource(R.string.reboot))
32 | }
33 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && context.getSystemService(PowerManager::class.java)?.isRebootingUserspaceSupported == true) {
34 | OutlinedButton(
35 | modifier = Modifier
36 | .fillMaxWidth(),
37 | shape = RoundedCornerShape(4.dp),
38 | onClick = { viewModel.rebootUserspace() }
39 | ) {
40 | Text(stringResource(R.string.reboot_userspace))
41 | }
42 | }
43 | OutlinedButton(
44 | modifier = Modifier
45 | .fillMaxWidth(),
46 | shape = RoundedCornerShape(4.dp),
47 | onClick = { viewModel.rebootRecovery() }
48 | ) {
49 | Text(stringResource(R.string.reboot_recovery))
50 | }
51 | OutlinedButton(
52 | modifier = Modifier
53 | .fillMaxWidth(),
54 | shape = RoundedCornerShape(4.dp),
55 | onClick = { viewModel.rebootBootloader() }
56 | ) {
57 | Text(stringResource(R.string.reboot_bootloader))
58 | }
59 | OutlinedButton(
60 | modifier = Modifier
61 | .fillMaxWidth(),
62 | shape = RoundedCornerShape(4.dp),
63 | onClick = { viewModel.rebootDownload() }
64 | ) {
65 | Text(stringResource(R.string.reboot_download))
66 | }
67 | OutlinedButton(
68 | modifier = Modifier
69 | .fillMaxWidth(),
70 | shape = RoundedCornerShape(4.dp),
71 | onClick = { viewModel.rebootEdl() }
72 | ) {
73 | Text(stringResource(R.string.reboot_edl))
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/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 | }
7 |
8 | android {
9 | compileSdk 34
10 |
11 | defaultConfig {
12 | applicationId "com.github.rimuruchan.kernelflasher"
13 | minSdk 29
14 | targetSdk 34
15 | versionCode 19
16 | versionName "1.0.0-alpha19"
17 |
18 | javaCompileOptions {
19 | annotationProcessorOptions {
20 | arguments += [
21 | "room.schemaLocation": "$projectDir/schemas".toString(),
22 | "room.incremental": "true"
23 | ]
24 | }
25 | }
26 | ndk {
27 | //noinspection ChromeOsAbiSupport
28 | abiFilters = ['armeabi-v7a', 'arm64-v8a']
29 | }
30 | vectorDrawables {
31 | useSupportLibrary true
32 | }
33 | }
34 | buildTypes {
35 | release {
36 | minifyEnabled false
37 | }
38 | }
39 | sourceSets {
40 | main {
41 | jniLibs.srcDirs = ['src/main/jniLibs']
42 | }
43 | }
44 | buildFeatures {
45 | aidl true
46 | }
47 | compileOptions {
48 | sourceCompatibility JavaVersion.VERSION_17
49 | targetCompatibility JavaVersion.VERSION_17
50 | }
51 | kotlinOptions {
52 | jvmTarget = '17'
53 | }
54 | buildFeatures {
55 | compose true
56 | }
57 | composeOptions {
58 | kotlinCompilerExtensionVersion libs.versions.compose.compiler.get()
59 | }
60 | packagingOptions {
61 | resources {
62 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
63 | }
64 | jniLibs {
65 | useLegacyPackaging true
66 | }
67 | }
68 | namespace 'com.github.rimuruchan.kernelflasher'
69 | }
70 |
71 | dependencies {
72 | implementation(libs.androidx.activity.compose)
73 | implementation(libs.androidx.appcompat)
74 | implementation(libs.androidx.compose.material)
75 | implementation(libs.androidx.compose.material3)
76 | implementation(libs.androidx.compose.foundation)
77 | implementation(libs.androidx.compose.ui)
78 | implementation(libs.androidx.core.ktx)
79 | implementation(libs.androidx.core.splashscreen)
80 | implementation(libs.androidx.lifecycle.runtime.ktx)
81 | implementation(libs.androidx.lifecycle.viewmodel.compose)
82 | implementation(libs.androidx.navigation.compose)
83 | implementation(libs.androidx.room.runtime)
84 | annotationProcessor(libs.androidx.room.compiler)
85 | ksp(libs.androidx.room.compiler)
86 | implementation(libs.libsu.core)
87 | implementation(libs.libsu.io)
88 | implementation(libs.libsu.nio)
89 | implementation(libs.libsu.service)
90 | implementation(libs.material)
91 | implementation(libs.okhttp)
92 | implementation(libs.kotlinx.serialization.json)
93 | }
--------------------------------------------------------------------------------
/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/java/com/github/rimuruchan/kernelflasher/ui/components/DataRow.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.width
7 | import androidx.compose.foundation.text.selection.SelectionContainer
8 | import androidx.compose.material3.MaterialTheme
9 | import androidx.compose.material3.Text
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.MutableState
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.runtime.setValue
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.layout.layout
19 | import androidx.compose.ui.text.TextStyle
20 | import androidx.compose.ui.text.style.TextOverflow
21 | import androidx.compose.ui.unit.dp
22 |
23 | @Composable
24 | fun DataRow(
25 | label: String,
26 | value: String,
27 | labelColor: Color = Color.Unspecified,
28 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium,
29 | valueColor: Color = Color.Unspecified,
30 | valueStyle: TextStyle = MaterialTheme.typography.titleSmall,
31 | mutableMaxWidth: MutableState? = null,
32 | clickable: Boolean = false,
33 | ) {
34 | Row {
35 | val modifier = if (mutableMaxWidth != null) {
36 | var maxWidth by mutableMaxWidth
37 | Modifier
38 | .layout { measurable, constraints ->
39 | val placeable = measurable.measure(constraints)
40 | maxWidth = maxOf(maxWidth, placeable.width)
41 | layout(width = maxWidth, height = placeable.height) {
42 | placeable.placeRelative(0, 0)
43 | }
44 | }
45 | .alignByBaseline()
46 | } else {
47 | Modifier
48 | .alignByBaseline()
49 | }
50 | Text(
51 | modifier = modifier,
52 | text = label,
53 | color = labelColor,
54 | style = labelStyle
55 | )
56 | Spacer(Modifier.width(8.dp))
57 | SelectionContainer(Modifier.alignByBaseline()) {
58 | var clicked by remember { mutableStateOf(false) }
59 | val modifier = if (clickable) {
60 | Modifier
61 | .clickable { clicked = !clicked }
62 | .alignByBaseline()
63 | } else {
64 | Modifier
65 | .alignByBaseline()
66 | }
67 | Text(
68 | modifier = modifier,
69 | text = value,
70 | color = valueColor,
71 | style = valueStyle,
72 | maxLines = if (clicked) Int.MAX_VALUE else 1,
73 | overflow = if (clicked) TextOverflow.Visible else TextOverflow.Ellipsis
74 | )
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/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 | 刷入 AK3 压缩包
30 | 刷入分区镜像
31 | 恢复
32 | 检查内核版本
33 | 挂载 Vendor DLKM
34 | 卸载 Vendor DLKM
35 | 映射 Vendor DLKM
36 | 取消映射 Vendor DLKM
37 | 迁移
38 | 没有找到备份
39 | 删除
40 | 添加
41 | 链接地址
42 | 版本
43 | 发布日期
44 | 更新日期
45 | 变更日志
46 | 检查更新
47 | 下载
48 | 重启
49 | 软重启
50 | 重启到 Recovery
51 | 重启到 Bootloader
52 | 重启到 Download
53 | 重启到 EDL
54 | 保存 AK3 日志
55 | 保存刷写日志
56 | 保存备份日志
57 | 保存恢复日志
58 | 将 AK3 包作为备份保存
59 | 备份类型
60 | 哈希值
61 | 旧的备份无法选择分区
62 | 正在加载,请稍后...
63 |
--------------------------------------------------------------------------------
/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 | 插槽 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 | 刷入 AK3 壓縮包
30 | 刷入分割槽映象
31 | 還原
32 | 檢查核心版本
33 | 掛載 Vendor DLKM
34 | 解除安裝 Vendor DLKM
35 | 對映 Vendor DLKM
36 | 取消對映 Vendor DLKM
37 | 遷移
38 | 沒有找到備份
39 | 刪除
40 | 新增
41 | 連結地址
42 | 版本
43 | 釋出日期
44 | 更新日期
45 | 變更日誌
46 | 檢查更新
47 | 下載
48 | 重啟
49 | 軟重啟
50 | 重啟到 Recovery
51 | 重啟到 Bootloader
52 | 重啟到 Download
53 | 重啟到 EDL
54 | 儲存 AK3 日誌
55 | 儲存刷寫日誌
56 | 儲存備份日誌
57 | 儲存還原日誌
58 | 將 AK3 包作為備份儲存
59 | 備份型別
60 | 雜湊值
61 | 舊的備份無法選擇分割槽
62 | 正在加載,請稍後...
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/components/SlotCard.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.material3.ExperimentalMaterial3Api
5 | import androidx.compose.material3.MaterialTheme
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.mutableIntStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.res.stringResource
10 | import androidx.compose.ui.text.font.FontFamily
11 | import androidx.compose.ui.text.font.FontWeight
12 | import androidx.navigation.NavController
13 | import com.github.rimuruchan.kernelflasher.R
14 | import com.github.rimuruchan.kernelflasher.ui.screens.slot.SlotViewModel
15 |
16 | @ExperimentalMaterial3Api
17 | @Composable
18 | fun SlotCard(
19 | title: String,
20 | viewModel: SlotViewModel,
21 | navController: NavController,
22 | isSlotScreen: Boolean = false,
23 | showDlkm: Boolean = true,
24 | ) {
25 | DataCard(
26 | title = title,
27 | button = {
28 | if (!isSlotScreen) {
29 | AnimatedVisibility(!viewModel.isRefreshing) {
30 | ViewButton {
31 | navController.navigate("slot${viewModel.slotSuffix}")
32 | }
33 | }
34 | }
35 | }
36 | ) {
37 | val cardWidth = remember { mutableIntStateOf(0) }
38 | DataRow(
39 | label = stringResource(R.string.boot_sha1),
40 | value = viewModel.sha1.substring(0, 8),
41 | valueStyle = MaterialTheme.typography.titleSmall.copy(
42 | fontFamily = FontFamily.Monospace,
43 | fontWeight = FontWeight.Medium
44 | ),
45 | mutableMaxWidth = cardWidth
46 | )
47 | AnimatedVisibility(!viewModel.isRefreshing && viewModel.kernelVersion != null) {
48 | DataRow(
49 | label = stringResource(R.string.kernel_version),
50 | value = if (viewModel.kernelVersion != null) viewModel.kernelVersion!! else "",
51 | mutableMaxWidth = cardWidth,
52 | clickable = true
53 | )
54 | }
55 | if (showDlkm && viewModel.hasVendorDlkm) {
56 | var vendorDlkmValue = stringResource(R.string.not_found)
57 | if (viewModel.isVendorDlkmMapped) {
58 | vendorDlkmValue = if (viewModel.isVendorDlkmMounted) {
59 | String.format(
60 | "%s, %s",
61 | stringResource(R.string.exists),
62 | stringResource(R.string.mounted)
63 | )
64 | } else {
65 | String.format(
66 | "%s, %s",
67 | stringResource(R.string.exists),
68 | stringResource(R.string.unmounted)
69 | )
70 | }
71 | }
72 | DataRow(
73 | stringResource(R.string.vendor_dlkm),
74 | vendorDlkmValue,
75 | mutableMaxWidth = cardWidth
76 | )
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "1.9.21"
3 | compose-compiler = "1.5.6"
4 |
5 | androidx-activity-compose = "1.8.2"
6 | androidx-appcompat = "1.6.1"
7 | androidx-compose = "1.5.4"
8 | androidx-compose-material3 = "1.1.2"
9 | androidx-core-ktx = "1.12.0"
10 | androidx-core-splashscreen = "1.0.1"
11 | androidx-lifecycle = "2.6.2"
12 | androidx-navigation-compose = "2.7.6"
13 | androidx-room = "2.6.1"
14 | kotlinx-serialization-json = "1.5.1"
15 | libsu = "5.2.1"
16 | material = "1.11.0"
17 | okhttp = "4.11.0"
18 |
19 | android-application = "8.2.0"
20 | devtools-ksp = "1.9.21-1.0.16"
21 |
22 | [libraries]
23 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
24 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidx-appcompat" }
25 | androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" }
26 | androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose" }
27 | androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
28 | androidx-compose-ui = { group = "androidx.compose.ui", name="ui", version.ref = "androidx-compose" }
29 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
30 | androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" }
31 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
32 | androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
33 | androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation-compose" }
34 | androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
35 | androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
36 | kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
37 | libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
38 | libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" }
39 | libsu-nio = { group = "com.github.topjohnwu.libsu", name = "nio", version.ref = "libsu" }
40 | libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" }
41 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
42 | okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
43 |
44 | [plugins]
45 | android-application = { id = "com.android.application", version.ref = "android-application" }
46 | devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" }
47 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
48 | kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
49 |
--------------------------------------------------------------------------------
/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 A
12 | Slot B
13 | Boot SHA1
14 | Vendor DLKM
15 | Exists
16 | Not Found
17 | Mounted
18 | Unmounted
19 | View
20 | Backups
21 | Save ramoops
22 | Save dmesg
23 | Save logcat
24 | Back
25 | Backup
26 | Updates
27 | Flash
28 | Flash AK3 Zip
29 | Flash Partition Image
30 | Restore
31 | Check Kernel Version
32 | Mount Vendor DLKM
33 | Unmount Vendor DLKM
34 | Map Vendor DLKM
35 | Unmap Vendor DLKM
36 | Migrate
37 | No backups found
38 | Delete
39 | Add
40 | URL
41 | Version
42 | Date Released
43 | Last Updated
44 | Changelog
45 | Check for Updates
46 | Download
47 | Reboot
48 | Soft Reboot
49 | Reboot to Recovery
50 | Reboot to Bootloader
51 | Reboot to Download
52 | Reboot to EDL
53 | Save AK3 Log
54 | Save Flash Log
55 | Save Backup Log
56 | Save Restore Log
57 | Save AK3 Zip as Backup
58 | Backup Type
59 | Hashes
60 | Partition selection unavailable for legacy backups
61 | Loading may take a few seconds, please wait...
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/types/room/updates/Update.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/java/com/github/rimuruchan/kernelflasher/ui/screens/updates/UpdatesContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.OutlinedButton
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableIntStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.font.FontStyle
21 | import androidx.compose.ui.unit.dp
22 | import androidx.navigation.NavController
23 | import com.github.rimuruchan.kernelflasher.R
24 | import com.github.rimuruchan.kernelflasher.common.types.room.updates.DateSerializer
25 | import com.github.rimuruchan.kernelflasher.ui.components.DataCard
26 | import com.github.rimuruchan.kernelflasher.ui.components.DataRow
27 | import com.github.rimuruchan.kernelflasher.ui.components.ViewButton
28 |
29 | @ExperimentalMaterial3Api
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(
54 | stringResource(R.string.version),
55 | update.kernelVersion,
56 | mutableMaxWidth = cardWidth
57 | )
58 | DataRow(
59 | stringResource(R.string.date_released),
60 | DateSerializer.formatter.format(update.kernelDate),
61 | mutableMaxWidth = cardWidth
62 | )
63 | DataRow(
64 | label = stringResource(R.string.last_updated),
65 | value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!),
66 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
67 | labelStyle = MaterialTheme.typography.labelMedium.copy(
68 | fontStyle = FontStyle.Italic
69 | ),
70 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
71 | valueStyle = MaterialTheme.typography.titleSmall.copy(
72 | fontStyle = FontStyle.Italic
73 | ),
74 | mutableMaxWidth = cardWidth
75 | )
76 | }
77 | }
78 | }
79 | AnimatedVisibility(!viewModel.isRefreshing) {
80 | Column {
81 | Spacer(Modifier.height(12.dp))
82 | OutlinedButton(
83 | modifier = Modifier
84 | .fillMaxWidth(),
85 | shape = RoundedCornerShape(4.dp),
86 | onClick = { navController.navigate("updates/add") }
87 | ) {
88 | Text(stringResource(R.string.add))
89 | }
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/main/MainContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.OutlinedButton
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.mutableIntStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalContext
18 | import androidx.compose.ui.res.stringResource
19 | import androidx.compose.ui.unit.dp
20 | import androidx.navigation.NavController
21 | import com.github.rimuruchan.kernelflasher.R
22 | import com.github.rimuruchan.kernelflasher.ui.components.DataCard
23 | import com.github.rimuruchan.kernelflasher.ui.components.DataRow
24 | import com.github.rimuruchan.kernelflasher.ui.components.SlotCard
25 |
26 | @ExperimentalMaterial3Api
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 | DataRow(
48 | stringResource(R.string.slot_suffix),
49 | viewModel.slotSuffix,
50 | mutableMaxWidth = cardWidth
51 | )
52 | }
53 | Spacer(Modifier.height(16.dp))
54 | SlotCard(
55 | title = stringResource(R.string.slot_a),
56 | viewModel = viewModel.slotA,
57 | navController = navController
58 | )
59 | Spacer(Modifier.height(16.dp))
60 | SlotCard(
61 | title = stringResource(R.string.slot_b),
62 | viewModel = viewModel.slotB,
63 | navController = navController
64 | )
65 | Spacer(Modifier.height(16.dp))
66 | AnimatedVisibility(!viewModel.isRefreshing) {
67 | OutlinedButton(
68 | modifier = Modifier
69 | .fillMaxWidth(),
70 | shape = RoundedCornerShape(4.dp),
71 | onClick = { navController.navigate("backups") }
72 | ) {
73 | Text(stringResource(R.string.backups))
74 | }
75 | }
76 | AnimatedVisibility(!viewModel.isRefreshing) {
77 | OutlinedButton(
78 | modifier = Modifier
79 | .fillMaxWidth(),
80 | shape = RoundedCornerShape(4.dp),
81 | onClick = { navController.navigate("updates") }
82 | ) {
83 | Text(stringResource(R.string.updates))
84 | }
85 | }
86 | if (viewModel.hasRamoops) {
87 | OutlinedButton(
88 | modifier = Modifier
89 | .fillMaxWidth(),
90 | shape = RoundedCornerShape(4.dp),
91 | onClick = { viewModel.saveRamoops(context) }
92 | ) {
93 | Text(stringResource(R.string.save_ramoops))
94 | }
95 | }
96 | OutlinedButton(
97 | modifier = Modifier
98 | .fillMaxWidth(),
99 | shape = RoundedCornerShape(4.dp),
100 | onClick = { viewModel.saveDmesg(context) }
101 | ) {
102 | Text(stringResource(R.string.save_dmesg))
103 | }
104 | OutlinedButton(
105 | modifier = Modifier
106 | .fillMaxWidth(),
107 | shape = RoundedCornerShape(4.dp),
108 | onClick = { viewModel.saveLogcat(context) }
109 | ) {
110 | Text(stringResource(R.string.save_logcat))
111 | }
112 | AnimatedVisibility(!viewModel.isRefreshing) {
113 | OutlinedButton(
114 | modifier = Modifier
115 | .fillMaxWidth(),
116 | shape = RoundedCornerShape(4.dp),
117 | onClick = { navController.navigate("reboot") }
118 | ) {
119 | Text(stringResource(R.string.reboot))
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/updates/UpdatesViewContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.OutlinedButton
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableIntStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.font.FontStyle
21 | import androidx.compose.ui.unit.dp
22 | import androidx.navigation.NavController
23 | import com.github.rimuruchan.kernelflasher.R
24 | import com.github.rimuruchan.kernelflasher.common.types.room.updates.DateSerializer
25 | import com.github.rimuruchan.kernelflasher.ui.components.DataCard
26 | import com.github.rimuruchan.kernelflasher.ui.components.DataRow
27 | import kotlinx.serialization.ExperimentalSerializationApi
28 |
29 | @ExperimentalMaterial3Api
30 | @ExperimentalSerializationApi
31 | @Composable
32 | fun ColumnScope.UpdatesViewContent(
33 | viewModel: UpdatesViewModel,
34 | navController: NavController
35 | ) {
36 | val context = LocalContext.current
37 | viewModel.currentUpdate?.let { currentUpdate ->
38 | DataCard(currentUpdate.kernelName) {
39 | val cardWidth = remember { mutableIntStateOf(0) }
40 | DataRow(
41 | stringResource(R.string.version),
42 | currentUpdate.kernelVersion,
43 | mutableMaxWidth = cardWidth
44 | )
45 | DataRow(
46 | stringResource(R.string.date_released),
47 | DateSerializer.formatter.format(currentUpdate.kernelDate),
48 | mutableMaxWidth = cardWidth
49 | )
50 | DataRow(
51 | label = stringResource(R.string.last_updated),
52 | value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!),
53 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
54 | labelStyle = MaterialTheme.typography.labelMedium.copy(
55 | fontStyle = FontStyle.Italic
56 | ),
57 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f),
58 | valueStyle = MaterialTheme.typography.titleSmall.copy(
59 | fontStyle = FontStyle.Italic,
60 | ),
61 | mutableMaxWidth = cardWidth
62 | )
63 | }
64 | AnimatedVisibility(!viewModel.isRefreshing) {
65 | Column {
66 | Spacer(Modifier.height(5.dp))
67 | OutlinedButton(
68 | modifier = Modifier
69 | .fillMaxWidth(),
70 | shape = RoundedCornerShape(4.dp),
71 | onClick = { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } }
72 | ) {
73 | Text(stringResource(R.string.changelog))
74 | }
75 | // TODO: add download progress indicator
76 | OutlinedButton(
77 | modifier = Modifier
78 | .fillMaxWidth(),
79 | shape = RoundedCornerShape(4.dp),
80 | onClick = { viewModel.downloadKernel(context) }
81 | ) {
82 | Text(stringResource(R.string.download))
83 | }
84 | OutlinedButton(
85 | modifier = Modifier
86 | .fillMaxWidth(),
87 | shape = RoundedCornerShape(4.dp),
88 | onClick = { viewModel.update() }
89 | ) {
90 | Text(stringResource(R.string.check_for_updates))
91 | }
92 | OutlinedButton(
93 | modifier = Modifier
94 | .fillMaxWidth(),
95 | shape = RoundedCornerShape(4.dp),
96 | onClick = { viewModel.delete { navController.popBackStack() } }
97 | ) {
98 | Text(stringResource(R.string.delete))
99 | }
100 | }
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/RefreshableScreen.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.ColumnScope
9 | import androidx.compose.foundation.layout.WindowInsets
10 | import androidx.compose.foundation.layout.WindowInsetsSides
11 | import androidx.compose.foundation.layout.asPaddingValues
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.navigationBars
15 | import androidx.compose.foundation.layout.only
16 | import androidx.compose.foundation.layout.padding
17 | import androidx.compose.foundation.layout.statusBars
18 | import androidx.compose.foundation.rememberScrollState
19 | import androidx.compose.foundation.verticalScroll
20 | import androidx.compose.material.ExperimentalMaterialApi
21 | import androidx.compose.material.icons.Icons
22 | import androidx.compose.material.icons.filled.ArrowBack
23 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
24 | import androidx.compose.material.pullrefresh.pullRefresh
25 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
26 | import androidx.compose.material3.ExperimentalMaterial3Api
27 | import androidx.compose.material3.Icon
28 | import androidx.compose.material3.IconButton
29 | import androidx.compose.material3.MaterialTheme
30 | import androidx.compose.material3.Scaffold
31 | import androidx.compose.material3.Text
32 | import androidx.compose.runtime.Composable
33 | import androidx.compose.ui.Alignment
34 | import androidx.compose.ui.Modifier
35 | import androidx.compose.ui.platform.LocalContext
36 | import androidx.compose.ui.res.stringResource
37 | import androidx.compose.ui.unit.dp
38 | import androidx.navigation.NavController
39 | import com.github.rimuruchan.kernelflasher.R
40 | import com.github.rimuruchan.kernelflasher.ui.screens.main.MainViewModel
41 | import kotlinx.serialization.ExperimentalSerializationApi
42 |
43 | @ExperimentalMaterialApi
44 | @ExperimentalMaterial3Api
45 | @ExperimentalSerializationApi
46 | @Composable
47 | fun RefreshableScreen(
48 | viewModel: MainViewModel,
49 | navController: NavController,
50 | swipeEnabled: Boolean = false,
51 | content: @Composable ColumnScope.() -> Unit
52 | ) {
53 | val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues()
54 | val navigationBars = WindowInsets.navigationBars.asPaddingValues()
55 | val context = LocalContext.current
56 | val state = rememberPullRefreshState(viewModel.isRefreshing, onRefresh = {
57 | viewModel.refresh(context)
58 | })
59 | Scaffold(
60 | topBar = {
61 | Box(
62 | Modifier
63 | .fillMaxWidth()
64 | .padding(statusBar)
65 | ) {
66 | if (navController.previousBackStackEntry != null) {
67 | AnimatedVisibility(
68 | !viewModel.isRefreshing,
69 | enter = fadeIn(),
70 | exit = fadeOut()
71 | ) {
72 | IconButton(
73 | onClick = { navController.popBackStack() },
74 | modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp)
75 | ) {
76 | Icon(
77 | Icons.Filled.ArrowBack,
78 | contentDescription = stringResource(R.string.back),
79 | tint = MaterialTheme.colorScheme.onSurface
80 | )
81 | }
82 | }
83 | }
84 | Box(
85 | Modifier
86 | .fillMaxWidth()
87 | .padding(16.dp)
88 | ) {
89 | Text(
90 | modifier = Modifier.align(Alignment.Center),
91 | text = stringResource(R.string.app_name),
92 | style = MaterialTheme.typography.headlineSmall
93 | )
94 | }
95 | }
96 | }
97 | ) { paddingValues ->
98 | Box(
99 | modifier = Modifier
100 | .padding(paddingValues)
101 | .pullRefresh(state, swipeEnabled)
102 | .fillMaxSize(),
103 | ) {
104 | Column(
105 | modifier = Modifier
106 | .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding())
107 | .fillMaxSize()
108 | .verticalScroll(rememberScrollState()),
109 | content = content
110 | )
111 | PullRefreshIndicator(
112 | viewModel.isRefreshing,
113 | state = state,
114 | modifier = Modifier.align(Alignment.TopCenter),
115 | backgroundColor = MaterialTheme.colorScheme.background,
116 | contentColor = MaterialTheme.colorScheme.primaryContainer,
117 | scale = true
118 | )
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/components/FlashList.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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/rimuruchan/kernelflasher/ui/screens/slot/SlotContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.OutlinedButton
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
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.ExperimentalUnitApi
19 | import androidx.compose.ui.unit.dp
20 | import androidx.navigation.NavController
21 | import com.github.rimuruchan.kernelflasher.R
22 | import com.github.rimuruchan.kernelflasher.ui.components.SlotCard
23 |
24 | @ExperimentalAnimationApi
25 | @ExperimentalMaterial3Api
26 | @ExperimentalUnitApi
27 | @Composable
28 | fun ColumnScope.SlotContent(
29 | viewModel: SlotViewModel,
30 | slotSuffix: String,
31 | navController: NavController
32 | ) {
33 | val context = LocalContext.current
34 | SlotCard(
35 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else R.string.slot_b),
36 | viewModel = viewModel,
37 | navController = navController,
38 | isSlotScreen = true
39 | )
40 | AnimatedVisibility(!viewModel.isRefreshing) {
41 | Column {
42 | Spacer(Modifier.height(5.dp))
43 | OutlinedButton(
44 | modifier = Modifier
45 | .fillMaxWidth(),
46 | shape = RoundedCornerShape(4.dp),
47 | onClick = {
48 | navController.navigate("slot$slotSuffix/flash")
49 | }
50 | ) {
51 | Text(stringResource(R.string.flash))
52 | }
53 | OutlinedButton(
54 | modifier = Modifier
55 | .fillMaxWidth(),
56 | shape = RoundedCornerShape(4.dp),
57 | onClick = {
58 | viewModel.clearFlash(context)
59 | navController.navigate("slot$slotSuffix/backup")
60 | }
61 | ) {
62 | Text(stringResource(R.string.backup))
63 | }
64 | OutlinedButton(
65 | modifier = Modifier
66 | .fillMaxWidth(),
67 | shape = RoundedCornerShape(4.dp),
68 | onClick = {
69 | navController.navigate("slot$slotSuffix/backups")
70 | }
71 | ) {
72 | Text(stringResource(R.string.restore))
73 | }
74 | OutlinedButton(
75 | modifier = Modifier
76 | .fillMaxWidth(),
77 | shape = RoundedCornerShape(4.dp),
78 | onClick = { if (!viewModel.isRefreshing) viewModel.getKernel(context) }
79 | ) {
80 | Text(stringResource(R.string.check_kernel_version))
81 | }
82 | if (viewModel.hasVendorDlkm) {
83 | AnimatedVisibility(!viewModel.isRefreshing) {
84 | AnimatedVisibility(viewModel.isVendorDlkmMounted) {
85 | OutlinedButton(
86 | modifier = Modifier
87 | .fillMaxWidth(),
88 | shape = RoundedCornerShape(4.dp),
89 | onClick = { viewModel.unmountVendorDlkm(context) }
90 | ) {
91 | Text(stringResource(R.string.unmount_vendor_dlkm))
92 | }
93 | }
94 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) {
95 | Column {
96 | OutlinedButton(
97 | modifier = Modifier
98 | .fillMaxWidth(),
99 | shape = RoundedCornerShape(4.dp),
100 | onClick = { viewModel.mountVendorDlkm(context) }
101 | ) {
102 | Text(stringResource(R.string.mount_vendor_dlkm))
103 | }
104 | OutlinedButton(
105 | modifier = Modifier
106 | .fillMaxWidth(),
107 | shape = RoundedCornerShape(4.dp),
108 | onClick = { viewModel.unmapVendorDlkm(context) }
109 | ) {
110 | Text(stringResource(R.string.unmap_vendor_dlkm))
111 | }
112 | }
113 | }
114 | AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) {
115 | OutlinedButton(
116 | modifier = Modifier
117 | .fillMaxWidth(),
118 | shape = RoundedCornerShape(4.dp),
119 | onClick = { viewModel.mapVendorDlkm(context) }
120 | ) {
121 | Text(stringResource(R.string.map_vendor_dlkm))
122 | }
123 | }
124 | }
125 | }
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/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/rimuruchan/kernelflasher/ui/screens/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.kernelflasher.common.PartitionUtil
13 | import com.github.rimuruchan.kernelflasher.common.types.backups.Backup
14 | import com.github.rimuruchan.kernelflasher.ui.screens.backups.BackupsViewModel
15 | import com.github.rimuruchan.kernelflasher.ui.screens.reboot.RebootViewModel
16 | import com.github.rimuruchan.kernelflasher.ui.screens.slot.SlotViewModel
17 | import com.github.rimuruchan.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 slotA: SlotViewModel
42 | val slotB: SlotViewModel
43 | val backups: BackupsViewModel
44 | val updates: UpdatesViewModel
45 | val reboot: RebootViewModel
46 | val hasRamoops: Boolean
47 |
48 | private val _isRefreshing: MutableState = mutableStateOf(true)
49 | private var _error: String? = null
50 | private var _backups: MutableMap = mutableMapOf()
51 |
52 | val isRefreshing: Boolean
53 | get() = _isRefreshing.value
54 | val hasError: Boolean
55 | get() = _error != null
56 | val error: String
57 | get() = _error!!
58 |
59 | init {
60 | PartitionUtil.init(context, fileSystemManager)
61 | val bootA = PartitionUtil.findPartitionBlockDevice(context, "boot", "_a")!!
62 | val bootB = PartitionUtil.findPartitionBlockDevice(context, "boot", "_b")!!
63 | val initBootA = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_a")
64 | val initBootB = PartitionUtil.findPartitionBlockDevice(context, "init_boot", "_b")
65 | kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0]
66 | slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0]
67 | backups =
68 | BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups)
69 | updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing)
70 | reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing)
71 | slotA = SlotViewModel(
72 | context,
73 | fileSystemManager,
74 | navController,
75 | _isRefreshing,
76 | slotSuffix == "_a",
77 | "_a",
78 | bootA,
79 | initBootA,
80 | _backups
81 | )
82 | if (slotA.hasError) {
83 | _error = slotA.error
84 | }
85 | slotB = SlotViewModel(
86 | context,
87 | fileSystemManager,
88 | navController,
89 | _isRefreshing,
90 | slotSuffix == "_b",
91 | "_b",
92 | bootB,
93 | initBootB,
94 | _backups
95 | )
96 | if (slotB.hasError) {
97 | _error = slotB.error
98 | }
99 |
100 | hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists()
101 | _isRefreshing.value = false
102 | }
103 |
104 | fun refresh(context: Context) {
105 | launch {
106 | slotA.refresh(context)
107 | slotB.refresh(context)
108 | backups.refresh(context)
109 | }
110 | }
111 |
112 | private fun launch(block: suspend () -> Unit) {
113 | viewModelScope.launch(Dispatchers.IO) {
114 | viewModelScope.launch(Dispatchers.Main) {
115 | _isRefreshing.value = true
116 | }
117 | try {
118 | block()
119 | } catch (e: Exception) {
120 | withContext(Dispatchers.Main) {
121 | Log.e(TAG, e.message, e)
122 | navController.navigate("error/${e.message}") {
123 | popUpTo("main")
124 | }
125 | }
126 | }
127 | viewModelScope.launch(Dispatchers.Main) {
128 | _isRefreshing.value = false
129 | }
130 | }
131 | }
132 |
133 | @Suppress("SameParameterValue")
134 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
135 | Log.d(TAG, message)
136 | if (!shouldThrow) {
137 | viewModelScope.launch(Dispatchers.Main) {
138 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
139 | }
140 | } else {
141 | throw Exception(message)
142 | }
143 | }
144 |
145 | fun saveRamoops(context: Context) {
146 | launch {
147 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
148 |
149 | @SuppressLint("SdCardPath")
150 | val ramoops = File("/sdcard/Download/console-ramoops--$now.log")
151 | Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec()
152 | if (ramoops.exists()) {
153 | log(context, "Saved ramoops to $ramoops")
154 | } else {
155 | log(context, "Failed to save $ramoops", shouldThrow = true)
156 | }
157 | }
158 | }
159 |
160 | fun saveDmesg(context: Context) {
161 | launch {
162 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
163 |
164 | @SuppressLint("SdCardPath")
165 | val dmesg = File("/sdcard/Download/dmesg--$now.log")
166 | Shell.cmd("dmesg > $dmesg").exec()
167 | if (dmesg.exists()) {
168 | log(context, "Saved dmesg to $dmesg")
169 | } else {
170 | log(context, "Failed to save $dmesg", shouldThrow = true)
171 | }
172 | }
173 | }
174 |
175 | fun saveLogcat(context: Context) {
176 | launch {
177 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
178 |
179 | @SuppressLint("SdCardPath")
180 | val logcat = File("/sdcard/Download/logcat--$now.log")
181 | Shell.cmd("logcat -d > $logcat").exec()
182 | if (logcat.exists()) {
183 | log(context, "Saved logcat to $logcat")
184 | } else {
185 | log(context, "Failed to save $logcat", shouldThrow = true)
186 | }
187 | }
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/backups/BackupsContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material3.ExperimentalMaterial3Api
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.OutlinedButton
13 | import androidx.compose.material3.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.mutableIntStateOf
16 | import androidx.compose.runtime.remember
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.platform.LocalContext
19 | import androidx.compose.ui.res.stringResource
20 | import androidx.compose.ui.text.font.FontFamily
21 | import androidx.compose.ui.text.font.FontStyle
22 | import androidx.compose.ui.text.font.FontWeight
23 | import androidx.compose.ui.text.style.TextAlign
24 | import androidx.compose.ui.unit.dp
25 | import androidx.navigation.NavController
26 | import com.github.rimuruchan.kernelflasher.R
27 | import com.github.rimuruchan.kernelflasher.common.PartitionUtil
28 | import com.github.rimuruchan.kernelflasher.ui.components.DataCard
29 | import com.github.rimuruchan.kernelflasher.ui.components.DataRow
30 | import com.github.rimuruchan.kernelflasher.ui.components.DataSet
31 | import com.github.rimuruchan.kernelflasher.ui.components.ViewButton
32 |
33 | @ExperimentalMaterial3Api
34 | @Composable
35 | fun ColumnScope.BackupsContent(
36 | viewModel: BackupsViewModel,
37 | navController: NavController
38 | ) {
39 | val context = LocalContext.current
40 | if (viewModel.currentBackup != null && viewModel.backups.containsKey(viewModel.currentBackup)) {
41 | DataCard(viewModel.currentBackup!!) {
42 | val cardWidth = remember { mutableIntStateOf(0) }
43 | val currentBackup = viewModel.backups.getValue(viewModel.currentBackup!!)
44 | DataRow(
45 | stringResource(R.string.backup_type),
46 | currentBackup.type,
47 | mutableMaxWidth = cardWidth
48 | )
49 | DataRow(
50 | stringResource(R.string.kernel_version),
51 | currentBackup.kernelVersion,
52 | mutableMaxWidth = cardWidth,
53 | clickable = true
54 | )
55 | if (currentBackup.type == "raw") {
56 | DataRow(
57 | label = stringResource(R.string.boot_sha1),
58 | value = currentBackup.bootSha1!!.substring(0, 8),
59 | valueStyle = MaterialTheme.typography.titleSmall.copy(
60 | fontFamily = FontFamily.Monospace,
61 | fontWeight = FontWeight.Medium
62 | ),
63 | mutableMaxWidth = cardWidth
64 | )
65 | if (currentBackup.hashes != null) {
66 | val hashWidth = remember { mutableIntStateOf(0) }
67 | DataSet(stringResource(R.string.hashes)) {
68 | for (partitionName in PartitionUtil.PartitionNames) {
69 | val hash = currentBackup.hashes.get(partitionName)
70 | if (hash != null) {
71 | DataRow(
72 | label = partitionName,
73 | value = hash.substring(0, 8),
74 | valueStyle = MaterialTheme.typography.titleSmall.copy(
75 | fontFamily = FontFamily.Monospace,
76 | fontWeight = FontWeight.Medium
77 | ),
78 | mutableMaxWidth = hashWidth
79 | )
80 | }
81 | }
82 | }
83 | }
84 | }
85 | }
86 | AnimatedVisibility(!viewModel.isRefreshing) {
87 | Column {
88 | Spacer(Modifier.height(5.dp))
89 | OutlinedButton(
90 | modifier = Modifier
91 | .fillMaxWidth(),
92 | shape = RoundedCornerShape(4.dp),
93 | onClick = { viewModel.delete(context) { navController.popBackStack() } }
94 | ) {
95 | Text(stringResource(R.string.delete))
96 | }
97 | }
98 | }
99 | } else {
100 | DataCard(stringResource(R.string.backups))
101 | AnimatedVisibility(viewModel.needsMigration) {
102 | Column {
103 | Spacer(Modifier.height(5.dp))
104 | OutlinedButton(
105 | modifier = Modifier
106 | .fillMaxWidth(),
107 | shape = RoundedCornerShape(4.dp),
108 | onClick = { viewModel.migrate(context) }
109 | ) {
110 | Text(stringResource(R.string.migrate))
111 | }
112 | }
113 | }
114 | if (viewModel.backups.isNotEmpty()) {
115 | for (id in viewModel.backups.keys.sortedByDescending { it }) {
116 | val currentBackup = viewModel.backups[id]!!
117 | Spacer(Modifier.height(16.dp))
118 | DataCard(
119 | title = id,
120 | button = {
121 | AnimatedVisibility(!viewModel.isRefreshing) {
122 | Column {
123 | ViewButton(onClick = {
124 | navController.navigate("backups/$id")
125 | })
126 | }
127 | }
128 | }
129 | ) {
130 | val cardWidth = remember { mutableIntStateOf(0) }
131 | if (currentBackup.type == "raw") {
132 | DataRow(
133 | label = stringResource(R.string.boot_sha1),
134 | value = currentBackup.bootSha1!!.substring(0, 8),
135 | valueStyle = MaterialTheme.typography.titleSmall.copy(
136 | fontFamily = FontFamily.Monospace,
137 | fontWeight = FontWeight.Medium
138 | ),
139 | mutableMaxWidth = cardWidth
140 | )
141 | }
142 | DataRow(
143 | stringResource(R.string.kernel_version),
144 | currentBackup.kernelVersion,
145 | mutableMaxWidth = cardWidth,
146 | clickable = true
147 | )
148 | }
149 | }
150 | } else {
151 | Spacer(Modifier.height(32.dp))
152 | Text(
153 | stringResource(R.string.no_backups_found),
154 | modifier = Modifier.fillMaxWidth(),
155 | textAlign = TextAlign.Center,
156 | fontStyle = FontStyle.Italic
157 | )
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/updates/UpdatesViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.kernelflasher.common.types.room.AppDatabase
18 | import com.github.rimuruchan.kernelflasher.common.types.room.updates.Update
19 | import com.github.rimuruchan.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 =
49 | Room.databaseBuilder(context, AppDatabase::class.java, "kernel-flasher").build()
50 | private val updateDao = db.updateDao()
51 | private val _updates: SnapshotStateList = mutableStateListOf()
52 |
53 | var currentUpdate: Update? = null
54 | var changelog: String? = null
55 |
56 | val updates: List
57 | get() = _updates
58 | val isRefreshing: Boolean
59 | get() = _isRefreshing.value
60 |
61 | init {
62 | launch {
63 | val updates = updateDao.getAll()
64 | viewModelScope.launch(Dispatchers.Main) {
65 | _updates.addAll(updates)
66 | }
67 | }
68 | }
69 |
70 | private fun launch(block: suspend () -> Unit) {
71 | viewModelScope.launch(Dispatchers.IO) {
72 | viewModelScope.launch(Dispatchers.Main) {
73 | _isRefreshing.value = true
74 | }
75 | try {
76 | block()
77 | } catch (e: Exception) {
78 | withContext(Dispatchers.Main) {
79 | Log.e(TAG, e.message, e)
80 | navController.navigate("error/${e.message}") {
81 | popUpTo("main")
82 | }
83 | }
84 | }
85 | viewModelScope.launch(Dispatchers.Main) {
86 | _isRefreshing.value = false
87 | }
88 | }
89 | }
90 |
91 | @Suppress("SameParameterValue")
92 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
93 | Log.d(TAG, message)
94 | if (!shouldThrow) {
95 | viewModelScope.launch(Dispatchers.Main) {
96 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
97 | }
98 | } else {
99 | throw Exception(message)
100 | }
101 | }
102 |
103 | fun clearCurrent() {
104 | currentUpdate = null
105 | changelog = null
106 | }
107 |
108 | fun add(url: String, callback: (updateId: Int) -> Unit) {
109 | launch {
110 | val request = Request.Builder()
111 | .url(url)
112 | .build()
113 |
114 | client.newCall(request).execute().use { response ->
115 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
116 | val update: Update =
117 | Json.decodeFromString(UpdateSerializer, response.body!!.string())
118 | update.updateUri = url
119 | update.lastUpdated = Date()
120 | val updateId = updateDao.insert(update).toInt()
121 | val inserted = updateDao.load(updateId)
122 | withContext(Dispatchers.Main) {
123 | _updates.add(inserted)
124 | callback.invoke(updateId)
125 | }
126 | }
127 | }
128 | }
129 |
130 | fun update() {
131 | launch {
132 | val request = Request.Builder()
133 | .url(currentUpdate!!.updateUri!!)
134 | .build()
135 |
136 | client.newCall(request).execute().use { response ->
137 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
138 | val update: Update =
139 | Json.decodeFromString(UpdateSerializer, response.body!!.string())
140 | currentUpdate!!.let {
141 | withContext(Dispatchers.Main) {
142 | it.kernelName = update.kernelName
143 | it.kernelVersion = update.kernelVersion
144 | it.kernelLink = update.kernelLink
145 | it.kernelChangelogUrl = update.kernelChangelogUrl
146 | it.kernelDate = update.kernelDate
147 | it.kernelSha1 = update.kernelSha1
148 | it.supportLink = update.supportLink
149 | it.lastUpdated = Date()
150 | viewModelScope.launch(Dispatchers.IO) {
151 | updateDao.update(it)
152 | }
153 | }
154 | }
155 | }
156 | }
157 | }
158 |
159 | fun downloadChangelog(callback: () -> Unit) {
160 | launch {
161 | val request = Request.Builder()
162 | .url(currentUpdate!!.kernelChangelogUrl)
163 | .build()
164 |
165 | client.newCall(request).execute().use { response ->
166 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
167 | changelog = response.body!!.string()
168 | withContext(Dispatchers.Main) {
169 | callback.invoke()
170 | }
171 | }
172 | }
173 | }
174 |
175 | private fun insertDownload(context: Context, filename: String): Uri? {
176 | val resolver = context.contentResolver
177 | val values = ContentValues()
178 | values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
179 | values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip")
180 | values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
181 | return resolver.insert(MediaStore.Files.getContentUri("external"), values)
182 | }
183 |
184 | fun downloadKernel(context: Context) {
185 | launch {
186 | val remoteUri = Uri.parse(currentUpdate!!.kernelLink)
187 | val filename = Path(remoteUri.path!!).name
188 | val localUri = insertDownload(context, filename)
189 | localUri!!.let { uri ->
190 | val request = Request.Builder()
191 | .url(remoteUri.toString())
192 | .build()
193 |
194 | client.newCall(request).execute().use { response ->
195 | if (!response.isSuccessful) throw IOException("Unexpected response: $response")
196 | response.body!!.byteStream().use { inputStream ->
197 | context.contentResolver.openOutputStream(uri)!!.use { outputStream ->
198 | inputStream.copyTo(outputStream)
199 | }
200 | }
201 | log(context, "Saved $filename to Downloads")
202 | }
203 | }
204 | }
205 | }
206 |
207 | fun delete(callback: () -> Unit) {
208 | launch {
209 | updateDao.delete(currentUpdate!!)
210 | withContext(Dispatchers.Main) {
211 | _updates.remove(currentUpdate!!)
212 | callback.invoke()
213 | currentUpdate = null
214 | }
215 | }
216 | }
217 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/common/PartitionUtil.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.common
2 |
3 | import android.content.Context
4 | import com.github.rimuruchan.kernelflasher.common.extensions.ByteArray.toHex
5 | import com.github.rimuruchan.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 = fileSystemManager!!.getFile(
76 | blockDevice.parentFile,
77 | "$partitionName$slotSuffix"
78 | )
79 | }
80 | }
81 | }
82 | if (blockDevice == null || !blockDevice.exists()) {
83 | val siblingDevice = if (bootParent != null) fileSystemManager!!.getFile(
84 | bootParent!!,
85 | "$partitionName$slotSuffix"
86 | ) else null
87 | val physicalDevice =
88 | fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix")
89 | val logicalDevice =
90 | fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix")
91 | if (siblingDevice?.exists() == true) {
92 | blockDevice = physicalDevice
93 | } else if (physicalDevice.exists()) {
94 | blockDevice = physicalDevice
95 | } else if (logicalDevice.exists()) {
96 | blockDevice = logicalDevice
97 | }
98 | }
99 | return blockDevice
100 | }
101 |
102 | @Suppress("unused")
103 | fun partitionAvb(context: Context, partitionName: String): String {
104 | val httools = File(context.filesDir, "httools_static")
105 | val result = Shell.cmd("$httools avb $partitionName").exec().out
106 | return if (result.isNotEmpty()) result[0] else ""
107 | }
108 |
109 | fun flashBlockDevice(
110 | image: ExtendedFile,
111 | blockDevice: ExtendedFile,
112 | hashAlgorithm: String
113 | ): String {
114 | // set device writable
115 | Shell.cmd("blockdev --setrw $blockDevice").exec()
116 | val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt()
117 | val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt()
118 | if (partitionSize < imageSize) {
119 | throw Error("Partition ${blockDevice.name} is smaller than image")
120 | }
121 | if (partitionSize > imageSize) {
122 | Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice").exec()
123 | }
124 | val messageDigest = MessageDigest.getInstance(hashAlgorithm)
125 | image.newInputStream().use { inputStream ->
126 | blockDevice.newOutputStream().use { outputStream ->
127 | DigestOutputStream(outputStream, messageDigest).use { digestOutputStream ->
128 | inputStream.copyTo(digestOutputStream)
129 | }
130 | }
131 | }
132 | return messageDigest.digest().toHex()
133 | }
134 |
135 | @Suppress("SameParameterValue")
136 | fun flashLogicalPartition(
137 | context: Context,
138 | image: ExtendedFile,
139 | blockDevice: ExtendedFile,
140 | partitionName: String,
141 | slotSuffix: String,
142 | hashAlgorithm: String,
143 | addMessage: (message: String) -> Unit
144 | ): String {
145 | val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt()
146 | val lptools = File(context.filesDir, "lptools_static")
147 | Shell.cmd("$lptools remove ${partitionName}_kf").exec()
148 | if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) {
149 | if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) {
150 | if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) {
151 | val temporaryBlockDevice =
152 | fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf")
153 | val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm)
154 | if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix")
155 | .exec().isSuccess
156 | ) {
157 | return hash
158 | } else {
159 | throw Error("Replacing $partitionName$slotSuffix failed")
160 | }
161 | } else {
162 | throw Error("Remapping ${partitionName}_kf failed")
163 | }
164 | } else {
165 | throw Error("Unmapping ${partitionName}_kf failed")
166 | }
167 | } else {
168 | addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...")
169 | val httools = File(context.filesDir, "httools_static")
170 | if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) {
171 | val verityBlockDevice =
172 | blockDevice.parentFile!!.getChildFile("${partitionName}-verity")
173 | if (verityBlockDevice.exists()) {
174 | if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) {
175 | throw Error("Unmapping ${partitionName}-verity failed")
176 | }
177 | }
178 | if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) {
179 | if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)")
180 | .exec().isSuccess
181 | ) {
182 | if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) {
183 | val hash = flashBlockDevice(image, blockDevice, hashAlgorithm)
184 | if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) {
185 | return hash
186 | } else {
187 | throw Error("Mounting $partitionName failed")
188 | }
189 | } else {
190 | throw Error("Remapping $partitionName$slotSuffix failed")
191 | }
192 | } else {
193 | throw Error("Resizing $partitionName$slotSuffix failed")
194 | }
195 | } else {
196 | throw Error("Unmapping $partitionName$slotSuffix failed")
197 | }
198 | } else {
199 | throw Error("Unmounting $partitionName failed")
200 | }
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/slot/SlotFlashContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher.ui.screens.slot
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.ColumnScope
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.offset
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.ExperimentalMaterialApi
14 | import androidx.compose.material3.ButtonDefaults
15 | import androidx.compose.material3.Checkbox
16 | import androidx.compose.material3.ExperimentalMaterial3Api
17 | import androidx.compose.material3.MaterialTheme
18 | import androidx.compose.material3.OutlinedButton
19 | import androidx.compose.material3.Text
20 | import androidx.compose.runtime.Composable
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.unit.ExperimentalUnitApi
28 | import androidx.compose.ui.unit.dp
29 | import androidx.navigation.NavController
30 | import com.github.rimuruchan.kernelflasher.R
31 | import com.github.rimuruchan.kernelflasher.common.PartitionUtil
32 | import com.github.rimuruchan.kernelflasher.ui.components.DataCard
33 | import com.github.rimuruchan.kernelflasher.ui.components.FlashButton
34 | import com.github.rimuruchan.kernelflasher.ui.components.FlashList
35 | import com.github.rimuruchan.kernelflasher.ui.components.SlotCard
36 |
37 | @ExperimentalAnimationApi
38 | @ExperimentalMaterialApi
39 | @ExperimentalMaterial3Api
40 | @ExperimentalUnitApi
41 | @Composable
42 | fun ColumnScope.SlotFlashContent(
43 | viewModel: SlotViewModel,
44 | slotSuffix: String,
45 | navController: NavController
46 | ) {
47 | val context = LocalContext.current
48 | if (!listOf(
49 | "/flash/ak3",
50 | "/flash/image/flash",
51 | "/backup/backup"
52 | ).any { navController.currentDestination!!.route!!.endsWith(it) }
53 | ) {
54 | SlotCard(
55 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else R.string.slot_b),
56 | viewModel = viewModel,
57 | navController = navController,
58 | isSlotScreen = true,
59 | showDlkm = false
60 | )
61 | Spacer(Modifier.height(16.dp))
62 | if (navController.currentDestination!!.route!! == "slot{slotSuffix}/flash") {
63 | DataCard(stringResource(R.string.flash))
64 | Spacer(Modifier.height(5.dp))
65 | FlashButton(stringResource(R.string.flash_ak3_zip), callback = { uri ->
66 | navController.navigate("slot$slotSuffix/flash/ak3") {
67 | popUpTo("slot{slotSuffix}")
68 | }
69 | viewModel.flashAk3(context, uri)
70 | })
71 | OutlinedButton(
72 | modifier = Modifier
73 | .fillMaxWidth(),
74 | shape = RoundedCornerShape(4.dp),
75 | onClick = {
76 | navController.navigate("slot$slotSuffix/flash/image")
77 | }
78 | ) {
79 | Text(stringResource(R.string.flash_partition_image))
80 | }
81 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/flash/image") {
82 | DataCard(stringResource(R.string.flash_partition_image))
83 | Spacer(Modifier.height(5.dp))
84 | for (partitionName in PartitionUtil.AvailablePartitions) {
85 | FlashButton(partitionName, callback = { uri ->
86 | navController.navigate("slot$slotSuffix/flash/image/flash") {
87 | popUpTo("slot{slotSuffix}")
88 | }
89 | viewModel.flashImage(context, uri, partitionName)
90 | })
91 | }
92 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup") {
93 | DataCard(stringResource(R.string.backup))
94 | Spacer(Modifier.height(5.dp))
95 | val disabledColor = ButtonDefaults.buttonColors(
96 | Color.Transparent,
97 | MaterialTheme.colorScheme.onSurface
98 | )
99 | for (partitionName in PartitionUtil.AvailablePartitions) {
100 | OutlinedButton(
101 | modifier = Modifier
102 | .fillMaxWidth()
103 | .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f),
104 | shape = RoundedCornerShape(4.dp),
105 | colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor,
106 | onClick = {
107 | viewModel.backupPartitions[partitionName] =
108 | !viewModel.backupPartitions[partitionName]!!
109 | },
110 | ) {
111 | Box(Modifier.fillMaxWidth()) {
112 | Checkbox(
113 | viewModel.backupPartitions[partitionName]!!, null,
114 | Modifier
115 | .align(Alignment.CenterStart)
116 | .offset(x = -(16.dp))
117 | )
118 | Text(partitionName, Modifier.align(Alignment.Center))
119 | }
120 | }
121 | }
122 | OutlinedButton(
123 | modifier = Modifier
124 | .fillMaxWidth(),
125 | shape = RoundedCornerShape(4.dp),
126 | onClick = {
127 | viewModel.backup(context)
128 | navController.navigate("slot$slotSuffix/backup/backup") {
129 | popUpTo("slot{slotSuffix}")
130 | }
131 | },
132 | enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty()
133 | ) {
134 | Text(stringResource(R.string.backup))
135 | }
136 | }
137 | } else {
138 | Text("")
139 | FlashList(
140 | stringResource(if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup/backup") R.string.backup else R.string.flash),
141 | if (navController.currentDestination!!.route!!.contains("ak3")) viewModel.uiPrintedOutput else viewModel.flashOutput
142 | ) {
143 | AnimatedVisibility(!viewModel.isRefreshing && viewModel.wasFlashSuccess != null) {
144 | Column {
145 | if (navController.currentDestination!!.route!!.contains("ak3")) {
146 | OutlinedButton(
147 | modifier = Modifier
148 | .fillMaxWidth(),
149 | shape = RoundedCornerShape(4.dp),
150 | onClick = { viewModel.saveLog(context) }
151 | ) {
152 | if (navController.currentDestination!!.route!!.contains("ak3")) {
153 | Text(stringResource(R.string.save_ak3_log))
154 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup/backup") {
155 | Text(stringResource(R.string.save_backup_log))
156 | } else {
157 | Text(stringResource(R.string.save_flash_log))
158 | }
159 | }
160 | }
161 | if (navController.currentDestination!!.route!!.contains("ak3")) {
162 | AnimatedVisibility(navController.currentDestination!!.route!! != "slot{slotSuffix}/backups/{backupId}/flash/ak3" && navController.previousBackStackEntry!!.destination.route!! != "slot{slotSuffix}/backups/{backupId}/flash/ak3" && viewModel.wasFlashSuccess != false) {
163 | OutlinedButton(
164 | modifier = Modifier
165 | .fillMaxWidth(),
166 | shape = RoundedCornerShape(4.dp),
167 | onClick = {
168 | viewModel.backupZip(context) {
169 | navController.navigate("slot$slotSuffix/backups") {
170 | popUpTo("slot{slotSuffix}")
171 | }
172 | }
173 | }
174 | ) {
175 | Text(stringResource(R.string.save_ak3_zip_as_backup))
176 | }
177 | }
178 | }
179 | if (viewModel.wasFlashSuccess != false && navController.currentDestination!!.route!! == "slot{slotSuffix}/backup/backup") {
180 | OutlinedButton(
181 | modifier = Modifier
182 | .fillMaxWidth(),
183 | shape = RoundedCornerShape(4.dp),
184 | onClick = { navController.popBackStack() }
185 | ) {
186 | Text(stringResource(R.string.back))
187 | }
188 | } else {
189 | OutlinedButton(
190 | modifier = Modifier
191 | .fillMaxWidth(),
192 | shape = RoundedCornerShape(4.dp),
193 | onClick = { navController.navigate("reboot") }
194 | ) {
195 | Text(stringResource(R.string.reboot))
196 | }
197 | }
198 | }
199 | }
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/backups/SlotBackupsContent.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.kernelflasher.R
35 | import com.github.rimuruchan.kernelflasher.common.PartitionUtil
36 | import com.github.rimuruchan.kernelflasher.ui.components.DataCard
37 | import com.github.rimuruchan.kernelflasher.ui.components.DataRow
38 | import com.github.rimuruchan.kernelflasher.ui.components.DataSet
39 | import com.github.rimuruchan.kernelflasher.ui.components.FlashList
40 | import com.github.rimuruchan.kernelflasher.ui.components.SlotCard
41 | import com.github.rimuruchan.kernelflasher.ui.components.ViewButton
42 | import com.github.rimuruchan.kernelflasher.ui.screens.slot.SlotViewModel
43 |
44 | @ExperimentalMaterial3Api
45 | @ExperimentalUnitApi
46 | @Composable
47 | fun ColumnScope.SlotBackupsContent(
48 | slotViewModel: SlotViewModel,
49 | backupsViewModel: BackupsViewModel,
50 | slotSuffix: String,
51 | navController: NavController
52 | ) {
53 | val context = LocalContext.current
54 | if (!navController.currentDestination!!.route!!.startsWith("slot{slotSuffix}/backups/{backupId}/restore")) {
55 | SlotCard(
56 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else R.string.slot_b),
57 | viewModel = slotViewModel,
58 | navController = navController,
59 | isSlotScreen = true,
60 | showDlkm = false,
61 | )
62 | Spacer(Modifier.height(16.dp))
63 | if (backupsViewModel.currentBackup != null && backupsViewModel.backups.containsKey(
64 | backupsViewModel.currentBackup
65 | )
66 | ) {
67 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!)
68 | DataCard(backupsViewModel.currentBackup!!) {
69 | val cardWidth = remember { mutableIntStateOf(0) }
70 | DataRow(
71 | stringResource(R.string.backup_type),
72 | currentBackup.type,
73 | mutableMaxWidth = cardWidth
74 | )
75 | DataRow(
76 | stringResource(R.string.kernel_version),
77 | currentBackup.kernelVersion,
78 | mutableMaxWidth = cardWidth,
79 | clickable = true
80 | )
81 | if (currentBackup.type == "raw") {
82 | DataRow(
83 | label = stringResource(R.string.boot_sha1),
84 | value = currentBackup.bootSha1!!.substring(0, 8),
85 | valueStyle = MaterialTheme.typography.titleSmall.copy(
86 | fontFamily = FontFamily.Monospace,
87 | fontWeight = FontWeight.Medium
88 | ),
89 | mutableMaxWidth = cardWidth
90 | )
91 | if (currentBackup.hashes != null) {
92 | val hashWidth = remember { mutableIntStateOf(0) }
93 | DataSet(stringResource(R.string.hashes)) {
94 | for (partitionName in PartitionUtil.PartitionNames) {
95 | val hash = currentBackup.hashes.get(partitionName)
96 | if (hash != null) {
97 | DataRow(
98 | label = partitionName,
99 | value = hash.substring(0, 8),
100 | valueStyle = MaterialTheme.typography.titleSmall.copy(
101 | fontFamily = FontFamily.Monospace,
102 | fontWeight = FontWeight.Medium
103 | ),
104 | mutableMaxWidth = hashWidth
105 | )
106 | }
107 | }
108 | }
109 | }
110 | }
111 | }
112 | AnimatedVisibility(!slotViewModel.isRefreshing) {
113 | Column {
114 | Spacer(Modifier.height(5.dp))
115 | if (slotViewModel.isActive) {
116 | if (currentBackup.type == "raw") {
117 | OutlinedButton(
118 | modifier = Modifier
119 | .fillMaxWidth(),
120 | shape = RoundedCornerShape(4.dp),
121 | onClick = {
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 | OutlinedButton(
129 | modifier = Modifier
130 | .fillMaxWidth(),
131 | shape = RoundedCornerShape(4.dp),
132 | onClick = {
133 | slotViewModel.flashAk3(
134 | context,
135 | backupsViewModel.currentBackup!!,
136 | currentBackup.filename!!
137 | )
138 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") {
139 | popUpTo("slot{slotSuffix}")
140 | }
141 | }
142 | ) {
143 | Text(stringResource(R.string.flash))
144 | }
145 | }
146 | }
147 | OutlinedButton(
148 | modifier = Modifier
149 | .fillMaxWidth(),
150 | shape = RoundedCornerShape(4.dp),
151 | onClick = { backupsViewModel.delete(context) { navController.popBackStack() } }
152 | ) {
153 | Text(stringResource(R.string.delete))
154 | }
155 | }
156 | }
157 | } else {
158 | DataCard(stringResource(R.string.backups))
159 | val backups =
160 | backupsViewModel.backups.filter { it.value.bootSha1.equals(slotViewModel.sha1) || it.value.type == "ak3" }
161 | if (backups.isNotEmpty()) {
162 | for (id in backups.keys.sortedByDescending { it }) {
163 | Spacer(Modifier.height(16.dp))
164 | DataCard(
165 | title = id,
166 | button = {
167 | AnimatedVisibility(!slotViewModel.isRefreshing) {
168 | ViewButton(onClick = {
169 | navController.navigate("slot$slotSuffix/backups/$id")
170 | })
171 | }
172 | }
173 | ) {
174 | DataRow(
175 | stringResource(R.string.kernel_version),
176 | backups[id]!!.kernelVersion,
177 | clickable = true
178 | )
179 | }
180 | }
181 | } else {
182 | Spacer(Modifier.height(32.dp))
183 | Text(
184 | stringResource(R.string.no_backups_found),
185 | modifier = Modifier.fillMaxWidth(),
186 | textAlign = TextAlign.Center,
187 | fontStyle = FontStyle.Italic
188 | )
189 | }
190 | }
191 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backups/{backupId}/restore") {
192 | DataCard(stringResource(R.string.restore))
193 | Spacer(Modifier.height(5.dp))
194 | val disabledColor = ButtonDefaults.buttonColors(
195 | Color.Transparent,
196 | MaterialTheme.colorScheme.onSurface
197 | )
198 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!)
199 | if (currentBackup.hashes != null) {
200 | for (partitionName in PartitionUtil.PartitionNames) {
201 | val hash = currentBackup.hashes.get(partitionName)
202 | if (hash != null) {
203 | OutlinedButton(
204 | modifier = Modifier
205 | .fillMaxWidth()
206 | .alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f),
207 | shape = RoundedCornerShape(4.dp),
208 | colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor,
209 | enabled = backupsViewModel.backupPartitions[partitionName] != null,
210 | onClick = {
211 | backupsViewModel.backupPartitions[partitionName] =
212 | !backupsViewModel.backupPartitions[partitionName]!!
213 | },
214 | ) {
215 | Box(Modifier.fillMaxWidth()) {
216 | Checkbox(
217 | backupsViewModel.backupPartitions[partitionName] == true, null,
218 | Modifier
219 | .align(Alignment.CenterStart)
220 | .offset(x = -(16.dp))
221 | )
222 | Text(partitionName, Modifier.align(Alignment.Center))
223 | }
224 | }
225 | }
226 | }
227 | } else {
228 | Text(
229 | stringResource(R.string.partition_selection_unavailable),
230 | modifier = Modifier.fillMaxWidth(),
231 | textAlign = TextAlign.Center,
232 | fontStyle = FontStyle.Italic
233 | )
234 | Spacer(Modifier.height(5.dp))
235 | }
236 | OutlinedButton(
237 | modifier = Modifier
238 | .fillMaxWidth(),
239 | shape = RoundedCornerShape(4.dp),
240 | onClick = {
241 | backupsViewModel.restore(context, slotSuffix)
242 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") {
243 | popUpTo("slot{slotSuffix}")
244 | }
245 | },
246 | enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none {
247 | currentBackup.hashes.get(
248 | it
249 | ) != null && backupsViewModel.backupPartitions[it] == null
250 | } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty())
251 | ) {
252 | Text(stringResource(R.string.restore))
253 | }
254 | } else {
255 | FlashList(
256 | stringResource(R.string.restore),
257 | backupsViewModel.restoreOutput
258 | ) {
259 | AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) {
260 | Column {
261 | if (backupsViewModel.wasRestored != false) {
262 | OutlinedButton(
263 | modifier = Modifier
264 | .fillMaxWidth(),
265 | shape = RoundedCornerShape(4.dp),
266 | onClick = { navController.navigate("reboot") }
267 | ) {
268 | Text(stringResource(R.string.reboot))
269 | }
270 | }
271 | }
272 | }
273 | }
274 | }
275 | }
276 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/ui/screens/backups/BackupsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.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.rimuruchan.kernelflasher.common.PartitionUtil
17 | import com.github.rimuruchan.kernelflasher.common.extensions.ExtendedFile.outputStream
18 | import com.github.rimuruchan.kernelflasher.common.extensions.ExtendedFile.readText
19 | import com.github.rimuruchan.kernelflasher.common.types.backups.Backup
20 | import com.github.rimuruchan.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.encodeToString
28 | import kotlinx.serialization.json.Json
29 | import java.io.File
30 | import java.io.FileInputStream
31 | import java.time.LocalDateTime
32 | import java.time.format.DateTimeFormatter
33 | import java.util.Properties
34 |
35 | class BackupsViewModel(
36 | context: Context,
37 | private val fileSystemManager: FileSystemManager,
38 | private val navController: NavController,
39 | private val _isRefreshing: MutableState,
40 | private val _backups: MutableMap
41 | ) : ViewModel() {
42 | companion object {
43 | const val TAG: String = "KernelFlasher/BackupsState"
44 | }
45 |
46 | private val _restoreOutput: SnapshotStateList = mutableStateListOf()
47 | var currentBackup: String? = null
48 | set(value) {
49 | if (value != field) {
50 | if (_backups[value]?.hashes != null) {
51 | PartitionUtil.AvailablePartitions.forEach { partitionName ->
52 | if (_backups[value]!!.hashes!!.get(partitionName) != null) {
53 | _backupPartitions[partitionName] = true
54 | }
55 | }
56 | }
57 | field = value
58 | }
59 | }
60 | var wasRestored: Boolean? = null
61 | private val _backupPartitions: SnapshotStateMap = mutableStateMapOf()
62 | private val hashAlgorithm: String = "SHA-256"
63 |
64 | @Deprecated("Backup migration will be removed in the first stable release")
65 | private var _needsMigration: MutableState = mutableStateOf(false)
66 |
67 | val restoreOutput: List
68 | get() = _restoreOutput
69 | val backupPartitions: MutableMap
70 | get() = _backupPartitions
71 | val isRefreshing: Boolean
72 | get() = _isRefreshing.value
73 | val backups: Map
74 | get() = _backups
75 |
76 | @Deprecated("Backup migration will be removed in the first stable release")
77 | val needsMigration: Boolean
78 | get() = _needsMigration.value
79 |
80 | init {
81 | refresh(context)
82 | }
83 |
84 | fun refresh(context: Context) {
85 | val oldDir = context.getExternalFilesDir(null)
86 | val oldBackupsDir = File(oldDir, "backups")
87 | @Deprecated("Backup migration will be removed in the first stable release")
88 | _needsMigration.value = oldBackupsDir.exists() && oldBackupsDir.listFiles()?.size!! > 0
89 | @SuppressLint("SdCardPath")
90 | val externalDir = File("/sdcard/KernelFlasher")
91 | val backupsDir = fileSystemManager.getFile("$externalDir/backups")
92 | if (backupsDir.exists()) {
93 | val children = backupsDir.listFiles()
94 | if (children != null) {
95 | for (child in children.sortedByDescending { it.name }) {
96 | if (!child.isDirectory) {
97 | continue
98 | }
99 | val jsonFile = child.getChildFile("backup.json")
100 | if (jsonFile.exists()) {
101 | _backups[child.name] = Json.decodeFromString(jsonFile.readText())
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
108 | private fun launch(block: suspend () -> Unit) {
109 | viewModelScope.launch(Dispatchers.IO) {
110 | _isRefreshing.value = true
111 | try {
112 | block()
113 | } catch (e: Exception) {
114 | withContext(Dispatchers.Main) {
115 | Log.e(TAG, e.message, e)
116 | navController.navigate("error/${e.message}") {
117 | popUpTo("main")
118 | }
119 | }
120 | }
121 | _isRefreshing.value = false
122 | }
123 | }
124 |
125 | @Suppress("SameParameterValue")
126 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) {
127 | Log.d(TAG, message)
128 | if (!shouldThrow) {
129 | viewModelScope.launch(Dispatchers.Main) {
130 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
131 | }
132 | } else {
133 | throw Exception(message)
134 | }
135 | }
136 |
137 | fun clearCurrent() {
138 | currentBackup = null
139 | clearRestore()
140 | }
141 |
142 | private fun addMessage(message: String) {
143 | viewModelScope.launch(Dispatchers.Main) {
144 | _restoreOutput.add(message)
145 | }
146 | }
147 |
148 | @Suppress("FunctionName")
149 | private fun _clearRestore() {
150 | _restoreOutput.clear()
151 | wasRestored = null
152 | }
153 |
154 | private fun clearRestore() {
155 | _clearRestore()
156 | _backupPartitions.clear()
157 | }
158 |
159 | @Suppress("unused")
160 | @SuppressLint("SdCardPath")
161 | fun saveLog(context: Context) {
162 | launch {
163 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm"))
164 | val log = File("/sdcard/Download/restore-log--$now.log")
165 | log.writeText(restoreOutput.joinToString("\n"))
166 | if (log.exists()) {
167 | log(context, "Saved restore log to $log")
168 | } else {
169 | log(context, "Failed to save $log", shouldThrow = true)
170 | }
171 | }
172 | }
173 |
174 | private fun restorePartitions(
175 | context: Context,
176 | source: ExtendedFile,
177 | slotSuffix: String
178 | ): Partitions? {
179 | val partitions = HashMap()
180 | for (partitionName in PartitionUtil.PartitionNames) {
181 | if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) {
182 | val image = source.getChildFile("$partitionName.img")
183 | if (image.exists()) {
184 | val blockDevice =
185 | PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix)
186 | if (blockDevice != null && blockDevice.exists()) {
187 | addMessage("Restoring $partitionName")
188 | partitions[partitionName] =
189 | if (PartitionUtil.isPartitionLogical(context, partitionName)) {
190 | PartitionUtil.flashLogicalPartition(
191 | context,
192 | image,
193 | blockDevice,
194 | partitionName,
195 | slotSuffix,
196 | hashAlgorithm
197 | ) { message ->
198 | addMessage(message)
199 | }
200 | } else {
201 | PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm)
202 | }
203 | } else {
204 | log(context, "Partition $partitionName was not found", shouldThrow = true)
205 | }
206 | }
207 | }
208 | }
209 | if (partitions.isNotEmpty()) {
210 | return Partitions.from(partitions)
211 | }
212 | return null
213 | }
214 |
215 | fun restore(context: Context, slotSuffix: String) {
216 | launch {
217 | _clearRestore()
218 | @SuppressLint("SdCardPath")
219 | val externalDir = File("/sdcard/KernelFlasher")
220 | val backupsDir = fileSystemManager.getFile("$externalDir/backups")
221 | val backupDir = backupsDir.getChildFile(currentBackup!!)
222 | if (!backupDir.exists()) {
223 | log(context, "Backup $currentBackup does not exists", shouldThrow = true)
224 | return@launch
225 | }
226 | addMessage("Restoring backup $currentBackup")
227 | val hashes = restorePartitions(context, backupDir, slotSuffix)
228 | if (hashes == null) {
229 | log(context, "No partitions restored", shouldThrow = true)
230 | }
231 | addMessage("Backup $currentBackup restored")
232 | wasRestored = true
233 | }
234 | }
235 |
236 | fun delete(context: Context, callback: () -> Unit) {
237 | launch {
238 | @SuppressLint("SdCardPath")
239 | val externalDir = File("/sdcard/KernelFlasher")
240 | val backupsDir = fileSystemManager.getFile("$externalDir/backups")
241 | val backupDir = backupsDir.getChildFile(currentBackup!!)
242 | if (!backupDir.exists()) {
243 | log(context, "Backup $currentBackup does not exists", shouldThrow = true)
244 | return@launch
245 | }
246 | backupDir.deleteRecursively()
247 | _backups.remove(currentBackup!!)
248 | withContext(Dispatchers.Main) {
249 | callback.invoke()
250 | }
251 | }
252 | }
253 |
254 | @SuppressLint("SdCardPath")
255 | @Deprecated("Backup migration will be removed in the first stable release")
256 | fun migrate(context: Context) {
257 | launch {
258 | val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher")
259 | if (!externalDir.exists()) {
260 | if (!externalDir.mkdir()) {
261 | log(
262 | context,
263 | "Failed to create KernelFlasher dir on /sdcard",
264 | shouldThrow = true
265 | )
266 | }
267 | }
268 | val backupsDir = externalDir.getChildFile("backups")
269 | if (!backupsDir.exists()) {
270 | if (!backupsDir.mkdir()) {
271 | log(context, "Failed to create backups dir", shouldThrow = true)
272 | }
273 | }
274 | val oldDir = context.getExternalFilesDir(null)
275 | val oldBackupsDir = File(oldDir, "backups")
276 | if (oldBackupsDir.exists()) {
277 | val indentedJson = Json { prettyPrint = true }
278 | val children = oldBackupsDir.listFiles()
279 | if (children != null) {
280 | for (child in children.sortedByDescending { it.name }) {
281 | if (!child.isDirectory) {
282 | child.delete()
283 | continue
284 | }
285 | val propFile = File(child, "backup.prop")
286 |
287 | @Suppress("BlockingMethodInNonBlockingContext")
288 | val inputStream = FileInputStream(propFile)
289 | val props = Properties()
290 | @Suppress("BlockingMethodInNonBlockingContext")
291 | props.load(inputStream)
292 |
293 | val name = child.name
294 | val type = props.getProperty("type", "raw")
295 | val kernelVersion = props.getProperty("kernel")
296 | val bootSha1 = if (type == "raw") props.getProperty("sha1") else null
297 | val filename = if (type == "ak3") "ak3.zip" else null
298 | propFile.delete()
299 |
300 | val dest = backupsDir.getChildFile(child.name)
301 | Shell.cmd("mv $child $dest").exec()
302 | if (!dest.exists()) {
303 | throw Error("Too slow")
304 | }
305 | val jsonFile = dest.getChildFile("backup.json")
306 | val backup = Backup(name, type, kernelVersion, bootSha1, filename)
307 | jsonFile.outputStream().use {
308 | it.write(
309 | indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)
310 | )
311 | }
312 | _backups[name] = backup
313 | }
314 | }
315 | oldBackupsDir.delete()
316 | }
317 | refresh(context)
318 | }
319 | }
320 | }
321 |
--------------------------------------------------------------------------------
/app/src/main/java/com/github/rimuruchan/kernelflasher/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.github.rimuruchan.kernelflasher
2 |
3 | import android.animation.ObjectAnimator
4 | import android.animation.PropertyValuesHolder
5 | import android.content.ComponentName
6 | import android.content.Intent
7 | import android.content.ServiceConnection
8 | import android.os.Bundle
9 | import android.os.IBinder
10 | import android.util.Log
11 | import android.view.View
12 | import android.view.ViewTreeObserver
13 | import android.view.animation.AccelerateInterpolator
14 | import android.widget.Toast
15 | import androidx.activity.ComponentActivity
16 | import androidx.activity.compose.BackHandler
17 | import androidx.activity.compose.setContent
18 | import androidx.compose.animation.AnimatedVisibilityScope
19 | import androidx.compose.animation.ExperimentalAnimationApi
20 | import androidx.compose.material.ExperimentalMaterialApi
21 | import androidx.compose.material3.ExperimentalMaterial3Api
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.ui.res.stringResource
24 | import androidx.compose.ui.unit.ExperimentalUnitApi
25 | import androidx.core.animation.doOnEnd
26 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
27 | import androidx.core.view.WindowCompat
28 | import androidx.lifecycle.ViewModelProvider
29 | import androidx.lifecycle.viewmodel.compose.viewModel
30 | import androidx.navigation.NavBackStackEntry
31 | import androidx.navigation.compose.NavHost
32 | import androidx.navigation.compose.composable
33 | import androidx.navigation.compose.rememberNavController
34 | import com.github.rimuruchan.kernelflasher.ui.screens.RefreshableScreen
35 | import com.github.rimuruchan.kernelflasher.ui.screens.backups.BackupsContent
36 | import com.github.rimuruchan.kernelflasher.ui.screens.backups.SlotBackupsContent
37 | import com.github.rimuruchan.kernelflasher.ui.screens.error.ErrorScreen
38 | import com.github.rimuruchan.kernelflasher.ui.screens.main.MainContent
39 | import com.github.rimuruchan.kernelflasher.ui.screens.main.MainViewModel
40 | import com.github.rimuruchan.kernelflasher.ui.screens.reboot.RebootContent
41 | import com.github.rimuruchan.kernelflasher.ui.screens.slot.SlotContent
42 | import com.github.rimuruchan.kernelflasher.ui.screens.slot.SlotFlashContent
43 | import com.github.rimuruchan.kernelflasher.ui.screens.updates.UpdatesAddContent
44 | import com.github.rimuruchan.kernelflasher.ui.screens.updates.UpdatesChangelogContent
45 | import com.github.rimuruchan.kernelflasher.ui.screens.updates.UpdatesContent
46 | import com.github.rimuruchan.kernelflasher.ui.screens.updates.UpdatesViewContent
47 | import com.github.rimuruchan.kernelflasher.ui.theme.KernelFlasherTheme
48 | import com.topjohnwu.superuser.Shell
49 | import com.topjohnwu.superuser.ipc.RootService
50 | import com.topjohnwu.superuser.nio.FileSystemManager
51 | import kotlinx.serialization.ExperimentalSerializationApi
52 | import java.io.File
53 |
54 |
55 | @ExperimentalAnimationApi
56 | @ExperimentalMaterialApi
57 | @ExperimentalMaterial3Api
58 | @ExperimentalSerializationApi
59 | @ExperimentalUnitApi
60 | class MainActivity : ComponentActivity() {
61 | companion object {
62 | const val TAG: String = "MainActivity"
63 |
64 | init {
65 | Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER))
66 | }
67 | }
68 |
69 | private var rootServiceConnected: Boolean = false
70 | private var viewModel: MainViewModel? = null
71 | private lateinit var mainListener: MainListener
72 | var isAwaitingResult = false
73 |
74 | inner class AidlConnection : ServiceConnection {
75 | override fun onServiceConnected(name: ComponentName, service: IBinder) {
76 | if (!rootServiceConnected) {
77 | val ipc: IFilesystemService = IFilesystemService.Stub.asInterface(service)
78 | val binder: IBinder = ipc.fileSystemService
79 | onAidlConnected(FileSystemManager.getRemote(binder))
80 | rootServiceConnected = true
81 | }
82 | }
83 |
84 | override fun onServiceDisconnected(name: ComponentName) {
85 | setContent {
86 | KernelFlasherTheme {
87 | ErrorScreen(stringResource(R.string.root_service_disconnected))
88 | }
89 | }
90 | }
91 | }
92 |
93 | private fun copyAsset(filename: String) {
94 | val dest = File(filesDir, filename)
95 | assets.open(filename).use { inputStream ->
96 | dest.outputStream().use { outputStream ->
97 | inputStream.copyTo(outputStream)
98 | }
99 | }
100 | Shell.cmd("chmod +x $dest").exec()
101 | }
102 |
103 | private fun copyNativeBinary(filename: String) {
104 | val binary = File(applicationInfo.nativeLibraryDir, "lib$filename.so")
105 | println("binary: $binary")
106 | val dest = File(filesDir, filename)
107 | println("dest: $dest")
108 | binary.inputStream().use { inputStream ->
109 | dest.outputStream().use { outputStream ->
110 | inputStream.copyTo(outputStream)
111 | }
112 | }
113 | Shell.cmd("chmod +x $dest").exec()
114 | }
115 |
116 | override fun onCreate(savedInstanceState: Bundle?) {
117 | WindowCompat.setDecorFitsSystemWindows(window, false)
118 | val splashScreen = installSplashScreen()
119 | super.onCreate(savedInstanceState)
120 |
121 | splashScreen.setOnExitAnimationListener { splashScreenView ->
122 | val scale = ObjectAnimator.ofPropertyValuesHolder(
123 | splashScreenView.view,
124 | PropertyValuesHolder.ofFloat(
125 | View.SCALE_X,
126 | 1f,
127 | 0f
128 | ),
129 | PropertyValuesHolder.ofFloat(
130 | View.SCALE_Y,
131 | 1f,
132 | 0f
133 | )
134 | )
135 | scale.interpolator = AccelerateInterpolator()
136 | scale.duration = 250L
137 | scale.doOnEnd { splashScreenView.remove() }
138 | scale.start()
139 | }
140 |
141 | Toast.makeText(this, getString(R.string.loading_please_wait), Toast.LENGTH_LONG)
142 | .show()
143 |
144 | val content: View = findViewById(android.R.id.content)
145 | content.viewTreeObserver.addOnPreDrawListener(
146 | object : ViewTreeObserver.OnPreDrawListener {
147 | override fun onPreDraw(): Boolean {
148 | return if (viewModel?.isRefreshing == false || Shell.isAppGrantedRoot() == false) {
149 | content.viewTreeObserver.removeOnPreDrawListener(this)
150 | true
151 | } else {
152 | false
153 | }
154 | }
155 | }
156 | )
157 |
158 | Shell.getShell()
159 | if (Shell.isAppGrantedRoot()!!) {
160 | val intent = Intent(this, FilesystemService::class.java)
161 | RootService.bind(intent, AidlConnection())
162 | } else {
163 | setContent {
164 | KernelFlasherTheme {
165 | ErrorScreen(stringResource(R.string.root_required))
166 | }
167 | }
168 | }
169 | }
170 |
171 | fun onAidlConnected(fileSystemManager: FileSystemManager) {
172 | try {
173 | Shell.cmd("cd $filesDir").exec()
174 | copyNativeBinary("lptools_static") // v20220825
175 | copyNativeBinary("httools_static") // v3.2.0
176 | copyNativeBinary("magiskboot") // v25.2
177 | copyAsset("flash_ak3.sh")
178 | } catch (e: Exception) {
179 | Log.e(TAG, e.message, e)
180 | setContent {
181 | KernelFlasherTheme {
182 | ErrorScreen(e.message!!)
183 | }
184 | }
185 | }
186 | setContent {
187 | val navController = rememberNavController()
188 | viewModel = viewModel {
189 | val application =
190 | checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY))
191 | MainViewModel(application, fileSystemManager, navController)
192 | }
193 | val mainViewModel = viewModel!!
194 | KernelFlasherTheme {
195 | if (!mainViewModel.hasError) {
196 | mainListener = MainListener {
197 | mainViewModel.refresh(this)
198 | }
199 | val slotViewModelA = mainViewModel.slotA
200 | val slotViewModelB = mainViewModel.slotB
201 | val backupsViewModel = mainViewModel.backups
202 | val updatesViewModel = mainViewModel.updates
203 | val rebootViewModel = mainViewModel.reboot
204 | BackHandler(enabled = mainViewModel.isRefreshing, onBack = {})
205 | val slotFlashContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit =
206 | { backStackEntry ->
207 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!!
208 | val slotViewModel =
209 | if (slotSuffix == "_a") slotViewModelA else slotViewModelB
210 | RefreshableScreen(mainViewModel, navController) {
211 | SlotFlashContent(slotViewModel, slotSuffix, navController)
212 | }
213 | }
214 | val slotBackupsContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit =
215 | { backStackEntry ->
216 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!!
217 | val slotViewModel =
218 | if (slotSuffix == "_a") slotViewModelA else slotViewModelB
219 | if (backStackEntry.arguments?.getString("backupId") != null) {
220 | backupsViewModel.currentBackup =
221 | backStackEntry.arguments?.getString("backupId")
222 | } else {
223 | backupsViewModel.clearCurrent()
224 | }
225 | RefreshableScreen(mainViewModel, navController) {
226 | SlotBackupsContent(
227 | slotViewModel,
228 | backupsViewModel,
229 | slotSuffix,
230 | navController
231 | )
232 | }
233 | }
234 | NavHost(navController = navController, startDestination = "main") {
235 | composable("main") {
236 | RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
237 | MainContent(mainViewModel, navController)
238 | }
239 | }
240 | composable("slot{slotSuffix}") { backStackEntry ->
241 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!!
242 | val slotViewModel =
243 | if (slotSuffix == "_a") slotViewModelA else slotViewModelB
244 | if (slotViewModel.wasFlashSuccess != null && navController.currentDestination!!.route.equals(
245 | "slot{slotSuffix}"
246 | )
247 | ) {
248 | slotViewModel.clearFlash(this@MainActivity)
249 | }
250 | RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
251 | SlotContent(slotViewModel, slotSuffix, navController)
252 | }
253 | }
254 | composable("slot{slotSuffix}/flash", content = slotFlashContent)
255 | composable("slot{slotSuffix}/flash/ak3", content = slotFlashContent)
256 | composable("slot{slotSuffix}/flash/image", content = slotFlashContent)
257 | composable("slot{slotSuffix}/flash/image/flash", content = slotFlashContent)
258 | composable("slot{slotSuffix}/backup", content = slotFlashContent)
259 | composable("slot{slotSuffix}/backup/backup", content = slotFlashContent)
260 | composable("slot{slotSuffix}/backups", content = slotBackupsContent)
261 | composable(
262 | "slot{slotSuffix}/backups/{backupId}",
263 | content = slotBackupsContent
264 | )
265 | composable(
266 | "slot{slotSuffix}/backups/{backupId}/restore",
267 | content = slotBackupsContent
268 | )
269 | composable(
270 | "slot{slotSuffix}/backups/{backupId}/restore/restore",
271 | content = slotBackupsContent
272 | )
273 | composable("slot{slotSuffix}/backups/{backupId}/flash/ak3") { backStackEntry ->
274 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!!
275 | val slotViewModel =
276 | if (slotSuffix == "_a") slotViewModelA else slotViewModelB
277 | backupsViewModel.currentBackup =
278 | backStackEntry.arguments?.getString("backupId")
279 | if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
280 | RefreshableScreen(mainViewModel, navController) {
281 | SlotFlashContent(slotViewModel, slotSuffix, navController)
282 | }
283 | }
284 | }
285 | composable("backups") {
286 | backupsViewModel.clearCurrent()
287 | RefreshableScreen(mainViewModel, navController) {
288 | BackupsContent(backupsViewModel, navController)
289 | }
290 | }
291 | composable("backups/{backupId}") { backStackEntry ->
292 | backupsViewModel.currentBackup =
293 | backStackEntry.arguments?.getString("backupId")
294 | if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) {
295 | RefreshableScreen(mainViewModel, navController) {
296 | BackupsContent(backupsViewModel, navController)
297 | }
298 | }
299 | }
300 | composable("updates") {
301 | updatesViewModel.clearCurrent()
302 | RefreshableScreen(mainViewModel, navController) {
303 | UpdatesContent(updatesViewModel, navController)
304 | }
305 | }
306 | composable("updates/add") {
307 | RefreshableScreen(mainViewModel, navController) {
308 | UpdatesAddContent(updatesViewModel, navController)
309 | }
310 | }
311 | composable("updates/view/{updateId}") { backStackEntry ->
312 | val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt()
313 | val currentUpdate =
314 | updatesViewModel.updates.firstOrNull { it.id == updateId }
315 | updatesViewModel.currentUpdate = currentUpdate
316 | if (updatesViewModel.currentUpdate != null) {
317 | // TODO: enable swipe refresh
318 | RefreshableScreen(mainViewModel, navController) {
319 | UpdatesViewContent(updatesViewModel, navController)
320 | }
321 | }
322 | }
323 | composable("updates/view/{updateId}/changelog") { backStackEntry ->
324 | val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt()
325 | val currentUpdate =
326 | updatesViewModel.updates.firstOrNull { it.id == updateId }
327 | updatesViewModel.currentUpdate = currentUpdate
328 | if (updatesViewModel.currentUpdate != null) {
329 | RefreshableScreen(mainViewModel, navController) {
330 | UpdatesChangelogContent(updatesViewModel, navController)
331 | }
332 | }
333 | }
334 | composable("reboot") {
335 | RefreshableScreen(mainViewModel, navController) {
336 | RebootContent(rebootViewModel, navController)
337 | }
338 | }
339 | composable("error/{error}") { backStackEntry ->
340 | val error = backStackEntry.arguments?.getString("error")
341 | ErrorScreen(error!!)
342 | }
343 | }
344 | } else {
345 | ErrorScreen(mainViewModel.error)
346 | }
347 | }
348 | }
349 | }
350 |
351 | public override fun onResume() {
352 | super.onResume()
353 | if (this::mainListener.isInitialized) {
354 | if (!isAwaitingResult) {
355 | mainListener.resume()
356 | }
357 | }
358 | }
359 | }
360 |
--------------------------------------------------------------------------------