Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# This repo is worked on from Windows where core.autocrlf=true. Shell scripts in
# assets/ are executed on-device by Android's /system/bin/sh, which cannot tolerate
# CRLF line endings (a trailing \r breaks every line). Force LF for all shell scripts
# and treat the prebuilt ELF binaries as binary so they are never line-ending converted.

*.sh text eol=lf

app/src/main/assets/flash_ak3.sh text eol=lf
app/src/main/assets/flash_ak3_mkbootfs.sh text eol=lf

app/src/main/assets/ksuinit binary
app/src/main/assets/mkbootfs binary
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ local.properties
.env
.github/workflows/build_local.yml
KernelFlasher.apk
/app/build/*
/app/build/*
# Release signing
key.properties
keystore/
32 changes: 30 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import java.util.Properties

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.devtools.ksp)
Expand All @@ -6,6 +8,13 @@ plugins {
alias(libs.plugins.kotlin.compose.compiler)
}

val keystoreProperties = Properties().apply {
val file = rootProject.file("key.properties")
if (file.exists()) {
file.inputStream().use { load(it) }
}
}

android {
compileSdk = 36
namespace = "com.github.capntrips.kernelflasher"
Expand Down Expand Up @@ -36,13 +45,31 @@ android {
}
}

signingConfigs {
if (keystoreProperties.isNotEmpty()) {
create("release") {
storeFile = rootProject.file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
}
}
}

buildTypes {
release {
isMinifyEnabled = false
isShrinkResources = false
// R8 minify + resource shrinking are ON to keep the APK small (mainly to
// tree-shake material-icons-extended). Keep rules in proguard-rules.pro cover
// AIDL/serialization/libsu/retrofit/gson, and -dontobfuscate protects Room and
// reflection-by-name. Verify core flows on-device after changing keep rules.
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
if (keystoreProperties.isNotEmpty()) {
signingConfig = signingConfigs.getByName("release")
}
}
}

Expand Down Expand Up @@ -95,6 +122,7 @@ android {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.icons.extended)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.core.ktx)
Expand Down
12 changes: 11 additions & 1 deletion app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@
-keepclassmembers class com.github.capntrips.kernelflasher.AppUpdater { *; }

# Keep VectorDrawableCompat to avoid crashes or inflation errors
-keep class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat { *; }
-keep class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat { *; }
# ============ KOTLINX.SERIALIZATION (app models: backups, partitions, updates) ============
-keepattributes InnerClasses
-keep,includedescriptorclasses class com.github.capntrips.kernelflasher.**$$serializer { *; }
-keepclassmembers class com.github.capntrips.kernelflasher.** {
*** Companion;
}
-keepclasseswithmembers class com.github.capntrips.kernelflasher.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep class com.github.capntrips.kernelflasher.common.types.** { *; }
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

<application
android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,23 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
import androidx.compose.material.icons.outlined.Layers
import androidx.compose.material.icons.outlined.SystemUpdateAlt
import androidx.compose.material3.TextButton
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
Expand All @@ -51,6 +61,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavBackStackEntry
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.github.capntrips.kernelflasher.ui.components.DialogButton
import com.github.capntrips.kernelflasher.ui.screens.RefreshableScreen
Expand All @@ -62,11 +73,8 @@ import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel
import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootContent
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotContent
import com.github.capntrips.kernelflasher.ui.screens.slot.SlotFlashContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesAddContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesChangelogContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesContent
import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewContent
import com.github.capntrips.kernelflasher.ui.theme.KernelFlasherTheme
import com.github.capntrips.kernelflasher.ui.theme.ThemePrefs
import com.topjohnwu.superuser.Shell
import com.topjohnwu.superuser.ipc.RootService
import com.topjohnwu.superuser.nio.FileSystemManager
Expand Down Expand Up @@ -144,6 +152,7 @@ class MainActivity : ComponentActivity() {
val splashScreen = installSplashScreen()
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
ThemePrefs.load(this)

val isZipIntent = intent?.action == Intent.ACTION_VIEW &&
(intent.type == "application/zip" || intent.data?.toString()?.endsWith(".zip") == true)
Expand Down Expand Up @@ -285,14 +294,8 @@ class MainActivity : ComponentActivity() {
val context = LocalContext.current
val dialogData = viewModel!!.updateDialogData
LaunchedEffect(Unit) {
if(AppUpdater.hasActiveInternetConnection()) {
AppUpdater.checkForUpdate(
context.applicationContext,
BuildConfig.VERSION_NAME
) { title, lines, confirm ->
viewModel!!.showUpdateDialog(title, lines, confirm)
}
}
// Self-update check disabled: this is a customised fork, and the upstream
// "Update APK" dialog would offer to replace this build with the stock one.

val uri = viewModel?.pendingFlashUri

Expand Down Expand Up @@ -334,11 +337,14 @@ class MainActivity : ComponentActivity() {
val slotViewModelA = mainViewModel.slotA
val slotViewModelB = mainViewModel.slotB
val backupsViewModel = mainViewModel.backups
val updatesViewModel = mainViewModel.updates
val rebootViewModel = mainViewModel.reboot
BackHandler(enabled = !mainViewModel.isRefreshing, onBack = {})
// New back handler for exit
BackHandler(enabled = true) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val isMainScreen = navBackStackEntry?.destination?.route == "main"
// Block back entirely while a root operation is in progress.
BackHandler(enabled = mainViewModel.isRefreshing) {}
// Only confirm-exit on the main screen. On any sub-page this handler is
// disabled, so back falls through to the NavHost and pops to the previous page.
BackHandler(enabled = isMainScreen && !mainViewModel.isRefreshing) {
showExitDialog = true
}
val slotContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
Expand Down Expand Up @@ -464,7 +470,26 @@ class MainActivity : ComponentActivity() {
}

}
NavHost(navController = navController, startDestination = "main") {
NavHost(
navController = navController,
startDestination = "main",
enterTransition = {
slideInHorizontally(initialOffsetX = { it / 4 }, animationSpec = tween(300)) +
fadeIn(tween(300))
},
exitTransition = {
slideOutHorizontally(targetOffsetX = { -it / 4 }, animationSpec = tween(300)) +
fadeOut(tween(300))
},
popEnterTransition = {
slideInHorizontally(initialOffsetX = { -it / 4 }, animationSpec = tween(300)) +
fadeIn(tween(300))
},
popExitTransition = {
slideOutHorizontally(targetOffsetX = { it / 4 }, animationSpec = tween(300)) +
fadeOut(tween(300))
}
) {
composable("main") {
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
MainContent(mainViewModel, navController)
Expand Down Expand Up @@ -524,38 +549,6 @@ class MainActivity : ComponentActivity() {
}
}
}
composable("updates") {
updatesViewModel.clearCurrent()
RefreshableScreen(mainViewModel, navController) {
UpdatesContent(updatesViewModel, navController)
}
}
composable("updates/add") {
RefreshableScreen(mainViewModel, navController) {
UpdatesAddContent(updatesViewModel, navController)
}
}
composable("updates/view/{updateId}") { backStackEntry ->
val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt()
val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId }
updatesViewModel.currentUpdate = currentUpdate
if (updatesViewModel.currentUpdate != null) {
// TODO: enable swipe refresh
RefreshableScreen(mainViewModel, navController) {
UpdatesViewContent(updatesViewModel, navController)
}
}
}
composable("updates/view/{updateId}/changelog") { backStackEntry ->
val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt()
val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId }
updatesViewModel.currentUpdate = currentUpdate
if (updatesViewModel.currentUpdate != null) {
RefreshableScreen(mainViewModel, navController) {
UpdatesChangelogContent(updatesViewModel, navController)
}
}
}
composable("reboot") {
RefreshableScreen(mainViewModel, navController) {
RebootContent(rebootViewModel, navController)
Expand All @@ -573,6 +566,7 @@ class MainActivity : ComponentActivity() {
if (dialogData != null) {
AlertDialog(
onDismissRequest = { viewModel!!.hideUpdateDialog() },
icon = { Icon(Icons.Outlined.SystemUpdateAlt, contentDescription = null) },
title = {
Text(
dialogData.title,
Expand Down Expand Up @@ -605,6 +599,7 @@ class MainActivity : ComponentActivity() {
if (showExitDialog) {
AlertDialog(
onDismissRequest = { showExitDialog = false },
icon = { Icon(Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null) },
title = { Text("Exit App") },
text = { Text("Are you sure you want to exit?") },
confirmButton = {
Expand All @@ -628,6 +623,7 @@ class MainActivity : ComponentActivity() {
if (viewModel?.showSlotIntentDialog?.value == true) {
AlertDialog(
onDismissRequest = { viewModel?.showSlotIntentDialog?.value = false },
icon = { Icon(Icons.Outlined.Layers, contentDescription = null) },
title = { Text("Select Slot to Flash") },
text = { Text("Choose the slot where the zip should be flashed.") },
confirmButton = {
Expand Down
Loading