Skip to content

Commit f08b6e9

Browse files
committed
Add Update Feature, Show Kernel and Ramdisk Format and Susfs verstion and Minor Changes
1. Add Feature to notify on new App releases in Github. 2. Show Kernel and Ramdisk (init_boot) Format. 3. Show Susfs version if susfsd present. 4. Update Back Press Handler. 5. Update flash_ak3.sh.
1 parent d013744 commit f08b6e9

File tree

23 files changed

+354
-43
lines changed

23 files changed

+354
-43
lines changed

.github/workflows/publish.yml

-6
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,6 @@ jobs:
5757
name: KernelFlasher
5858
path: KernelFlasher.apk
5959

60-
- name: Rename apk
61-
run: |
62-
ls -al
63-
DATE=$(date +'%y.%m.%d')
64-
echo "TAG=$DATE" >> $GITHUB_ENV
65-
6660
- name: Upload release
6761
uses: ncipollo/[email protected]
6862
with:

app/.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
/build
1+
/build
2+
/release
3+
build.gradle.bak
4+
*.bak

app/build.gradle

+4-3
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@ plugins {
88

99
android {
1010
compileSdk 35
11-
1211
defaultConfig {
1312
applicationId "com.github.capntrips.kernelflasher"
1413
minSdk 29
1514
targetSdk 34
16-
versionCode 21
17-
versionName "1.0.0-alpha21"
15+
versionCode 25
16+
versionName "1.4.0"
1817

1918
javaCompileOptions {
2019
annotationProcessorOptions {
@@ -91,4 +90,6 @@ dependencies {
9190
implementation(libs.material)
9291
implementation(libs.okhttp)
9392
implementation(libs.kotlinx.serialization.json)
93+
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
94+
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
9495
}

app/src/main/AndroidManifest.xml

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<uses-permission android:name="android.permission.INTERNET" />
6+
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
67

78
<application
89
android:allowBackup="true"

app/src/main/assets/flash_ak3.sh

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@ $F/busybox chown root:root $F/busybox $F/update-binary;
1111

1212
TMP=$F/tmp;
1313

14+
$F/busybox umount $TMP 2>/dev/null;
1415
$F/busybox rm -rf $TMP 2>/dev/null;
1516
$F/busybox mkdir -p $TMP;
1617

18+
$F/busybox mount -t tmpfs -o noatime tmpfs $TMP;
19+
#$F/busybox mount | $F/busybox grep -q " $TMP " || exit 1;
20+
1721
# update-binary <RECOVERY_API_VERSION> <OUTFD> <ZIPFILE>
1822
AKHOME=$TMP/anykernel $F/busybox ash $F/update-binary 3 1 "$Z";
1923
RC=$?;
2024

25+
$F/busybox umount $TMP;
2126
$F/busybox rm -rf $TMP;
2227
$F/busybox mount -o ro,remount -t auto /;
2328
$F/busybox rm -f $F/update-binary $F/busybox;
2429

2530
# work around libsu not cleanly accepting return or exit as last line
2631
safereturn() { return $RC; }
27-
safereturn;
32+
safereturn;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.github.capntrips.kernelflasher
2+
3+
import android.annotation.SuppressLint
4+
import android.app.DownloadManager
5+
import android.content.BroadcastReceiver
6+
import android.content.Context
7+
import android.content.Intent
8+
import android.content.IntentFilter
9+
import android.net.Uri
10+
import android.os.Environment
11+
import android.widget.Toast
12+
import com.google.gson.Gson
13+
import com.google.gson.annotations.SerializedName
14+
import retrofit2.Response
15+
import retrofit2.Retrofit
16+
import retrofit2.converter.gson.GsonConverterFactory
17+
import retrofit2.http.GET
18+
19+
interface GitHubApi {
20+
@GET("repos/fatalcoder524/KernelFlasher/releases/latest")
21+
suspend fun getLatestRelease(): Response<AppUpdater.GitHubRelease>
22+
}
23+
24+
object AppUpdater {
25+
26+
data class GitHubAsset(
27+
val name: String,
28+
@SerializedName("browser_download_url") val downloadUrl: String
29+
)
30+
31+
data class GitHubRelease(
32+
@SerializedName("tag_name") val tagName: String,
33+
val body: String,
34+
val assets: List<GitHubAsset>
35+
)
36+
37+
private val api: GitHubApi = Retrofit.Builder()
38+
.baseUrl("https://api.github.com/")
39+
.addConverterFactory(GsonConverterFactory.create(Gson()))
40+
.build()
41+
.create(GitHubApi::class.java)
42+
43+
// Compares version strings (e.g., v1.0.0 vs. v1.0.1)
44+
private fun isNewer(latest: String, current: String): Boolean {
45+
val latestParts = latest.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
46+
val currentParts = current.removePrefix("v").split(".").map { it.toIntOrNull() ?: 0 }
47+
48+
return latestParts.zip(currentParts).any { (l, c) -> l > c }
49+
}
50+
51+
52+
// Checks if an update is available
53+
suspend fun checkForUpdate(
54+
context: Context,
55+
currentVersion: String,
56+
onShowDialog: (String, List<String>, () -> Unit) -> Unit
57+
) {
58+
val response = api.getLatestRelease()
59+
if (response.isSuccessful) {
60+
val release = response.body() ?: return
61+
val latestVersion = release.tagName.removePrefix("v")
62+
if (isNewer(latestVersion, currentVersion)) {
63+
val apk = release.assets.find { it.name.endsWith(".apk") } ?: return
64+
val dialogTitle = "New version: $latestVersion"
65+
val dialogLines = listOf(
66+
"Changelog:",
67+
*release.body.split("\n").toTypedArray()
68+
)
69+
val confirmAction = { downloadAndInstallApk(context, apk.downloadUrl, latestVersion) }
70+
onShowDialog(dialogTitle, dialogLines, confirmAction)
71+
}
72+
}
73+
}
74+
75+
@SuppressLint("UnspecifiedRegisterReceiverFlag")
76+
private fun downloadAndInstallApk(context: Context, url: String, latestVersion: String) {
77+
Toast.makeText(context, "Downloading Update in Background. Don't perform any operations till update is completed!", Toast.LENGTH_LONG).show()
78+
val request = DownloadManager.Request(Uri.parse(url))
79+
request.setTitle("Kernel Flasher Latest Download")
80+
request.setDescription("Downloading update...")
81+
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Kernel_Flasher_$latestVersion.apk")
82+
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
83+
84+
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
85+
val id = manager.enqueue(request)
86+
87+
val receiver = object : BroadcastReceiver() {
88+
override fun onReceive(c: Context?, intent: Intent?) {
89+
val downloadId = intent?.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
90+
if (id == downloadId) {
91+
val apkUri = manager.getUriForDownloadedFile(id)
92+
val installIntent = Intent(Intent.ACTION_VIEW).apply {
93+
setDataAndType(apkUri, "application/vnd.android.package-archive")
94+
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
95+
}
96+
context.startActivity(installIntent)
97+
}
98+
}
99+
}
100+
101+
val appContext = context.applicationContext
102+
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
103+
104+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
105+
appContext.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
106+
} else {
107+
@Suppress("DEPRECATION")
108+
appContext.registerReceiver(receiver, intentFilter)
109+
}
110+
}
111+
}

app/src/main/java/com/github/capntrips/kernelflasher/MainActivity.kt

+96-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.github.capntrips.kernelflasher
22

33
import android.animation.ObjectAnimator
44
import android.animation.PropertyValuesHolder
5+
import android.app.Activity
56
import android.content.ComponentName
67
import android.content.Intent
78
import android.content.ServiceConnection
@@ -16,11 +17,27 @@ import androidx.activity.compose.BackHandler
1617
import androidx.activity.compose.setContent
1718
import androidx.compose.animation.AnimatedVisibilityScope
1819
import androidx.compose.animation.ExperimentalAnimationApi
20+
import androidx.compose.foundation.layout.Arrangement
21+
import androidx.compose.foundation.layout.Column
22+
import androidx.compose.foundation.layout.padding
1923
import androidx.compose.material.ExperimentalMaterialApi
24+
import androidx.compose.material.TextButton
25+
import androidx.compose.material3.AlertDialog
2026
import androidx.compose.material3.ExperimentalMaterial3Api
27+
import androidx.compose.material3.MaterialTheme
28+
import androidx.compose.material3.Text
2129
import androidx.compose.runtime.Composable
30+
import androidx.compose.runtime.LaunchedEffect
31+
import androidx.compose.runtime.getValue
32+
import androidx.compose.runtime.mutableStateOf
33+
import androidx.compose.runtime.remember
34+
import androidx.compose.runtime.setValue
35+
import androidx.compose.ui.Modifier
36+
import androidx.compose.ui.platform.LocalContext
2237
import androidx.compose.ui.res.stringResource
38+
import androidx.compose.ui.text.font.FontWeight
2339
import androidx.compose.ui.unit.ExperimentalUnitApi
40+
import androidx.compose.ui.unit.dp
2441
import androidx.core.animation.doOnEnd
2542
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
2643
import androidx.core.view.WindowCompat
@@ -30,6 +47,7 @@ import androidx.navigation.NavBackStackEntry
3047
import androidx.navigation.compose.NavHost
3148
import androidx.navigation.compose.composable
3249
import androidx.navigation.compose.rememberNavController
50+
import com.github.capntrips.kernelflasher.ui.components.DialogButton
3351
import com.github.capntrips.kernelflasher.ui.screens.RefreshableScreen
3452
import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsContent
3553
import com.github.capntrips.kernelflasher.ui.screens.backups.SlotBackupsContent
@@ -49,7 +67,7 @@ import com.topjohnwu.superuser.ipc.RootService
4967
import com.topjohnwu.superuser.nio.FileSystemManager
5068
import kotlinx.serialization.ExperimentalSerializationApi
5169
import java.io.File
52-
70+
import kotlin.system.exitProcess
5371

5472
@ExperimentalAnimationApi
5573
@ExperimentalMaterialApi
@@ -185,6 +203,20 @@ class MainActivity : ComponentActivity() {
185203
MainViewModel(application, fileSystemManager, navController)
186204
}
187205
val mainViewModel = viewModel!!
206+
207+
val context = LocalContext.current
208+
val dialogData = viewModel!!.updateDialogData
209+
LaunchedEffect(Unit) {
210+
AppUpdater.checkForUpdate(
211+
context.applicationContext,
212+
BuildConfig.VERSION_NAME
213+
) { title, lines, confirm ->
214+
viewModel!!.showUpdateDialog(title, lines, confirm)
215+
}
216+
}
217+
218+
var showExitDialog by remember { mutableStateOf(false) }
219+
188220
KernelFlasherTheme {
189221
if (!mainViewModel.hasError) {
190222
mainListener = MainListener {
@@ -195,11 +227,15 @@ class MainActivity : ComponentActivity() {
195227
val backupsViewModel = mainViewModel.backups
196228
val updatesViewModel = mainViewModel.updates
197229
val rebootViewModel = mainViewModel.reboot
198-
BackHandler(enabled = mainViewModel.isRefreshing, onBack = {})
230+
BackHandler(enabled = !mainViewModel.isRefreshing, onBack = {})
231+
// New back handler for exit
232+
BackHandler(enabled = true) {
233+
showExitDialog = true
234+
}
199235
val slotContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
200236
val slotSuffix = "_a"
201237
val slotViewModel = slotViewModelA
202-
if (slotViewModel!!.wasFlashSuccess != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
238+
if (slotViewModel!!.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
203239
slotViewModel.clearFlash(this@MainActivity)
204240
}
205241
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
@@ -210,7 +246,7 @@ class MainActivity : ComponentActivity() {
210246
val slotContentB: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
211247
val slotSuffix = "_b"
212248
val slotViewModel = slotViewModelB
213-
if (slotViewModel!!.wasFlashSuccess != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
249+
if (slotViewModel!!.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
214250
slotViewModel.clearFlash(this@MainActivity)
215251
}
216252
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
@@ -221,7 +257,7 @@ class MainActivity : ComponentActivity() {
221257
val slotContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry ->
222258
val slotSuffix = ""
223259
val slotViewModel = slotViewModelA
224-
if (slotViewModel!!.wasFlashSuccess != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
260+
if (slotViewModel!!.wasFlashSuccess.value != null && listOf("slot{slotSuffix}", "slot").any { navController.currentDestination!!.route.equals(it) }) {
225261
slotViewModel.clearFlash(this@MainActivity)
226262
}
227263
RefreshableScreen(mainViewModel, navController, swipeEnabled = true) {
@@ -424,6 +460,61 @@ class MainActivity : ComponentActivity() {
424460
} else {
425461
ErrorScreen(mainViewModel.error)
426462
}
463+
464+
if (dialogData != null) {
465+
AlertDialog(
466+
onDismissRequest = { viewModel!!.hideUpdateDialog() },
467+
title = {
468+
Text(
469+
dialogData!!.title,
470+
style = MaterialTheme.typography.titleLarge,
471+
fontWeight = FontWeight.Bold
472+
)
473+
},
474+
text = {
475+
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
476+
dialogData!!.changelog.forEach {
477+
Text(it, fontWeight = FontWeight.Bold)
478+
}
479+
}
480+
},
481+
confirmButton = {
482+
DialogButton("Update APK") {
483+
viewModel!!.hideUpdateDialog()
484+
dialogData!!.onConfirm()
485+
}
486+
},
487+
dismissButton = {
488+
DialogButton("CANCEL") {
489+
viewModel!!.hideUpdateDialog()
490+
}
491+
},
492+
modifier = Modifier.padding(16.dp)
493+
)
494+
}
495+
496+
if (showExitDialog) {
497+
AlertDialog(
498+
onDismissRequest = { showExitDialog = false },
499+
title = { Text("Exit App") },
500+
text = { Text("Are you sure you want to exit?") },
501+
confirmButton = {
502+
TextButton(onClick = {
503+
(context as? Activity)?.let {
504+
it.finishAffinity()
505+
exitProcess(0)
506+
}
507+
}) {
508+
Text("Yes")
509+
}
510+
},
511+
dismissButton = {
512+
TextButton(onClick = { showExitDialog = false }) {
513+
Text("No")
514+
}
515+
}
516+
)
517+
}
427518
}
428519
}
429520
}

app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ object PartitionUtil {
4747
private fun findPartitionFstabEntry(context: Context, partitionName: String): FstabEntry? {
4848
val httools = File(context.filesDir, "httools_static")
4949
val result = Shell.cmd("$httools dump $partitionName").exec().out
50-
if (result.isNotEmpty()) {
50+
if (result.isNotEmpty() && result[0].trim().startsWith("{")) {
5151
return Json.decodeFromString<FstabEntry>(result[0])
5252
}
5353
return null

app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt

-2
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import androidx.compose.material3.TextButton
99
import androidx.compose.material3.MaterialTheme
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.Modifier
12-
import androidx.compose.ui.res.stringResource
1312
import androidx.compose.ui.unit.LayoutDirection
1413
import androidx.compose.ui.unit.dp
15-
import com.github.capntrips.kernelflasher.R
1614

1715
@Composable
1816
fun DialogButton(

0 commit comments

Comments
 (0)