Skip to content

Commit 2c3e828

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 2c3e828

File tree

24 files changed

+390
-38
lines changed

24 files changed

+390
-38
lines changed

.github/workflows/publish.yml

Lines changed: 0 additions & 6 deletions
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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
/build
1+
/build
2+
/release

app/build.gradle

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,22 @@ plugins {
77
}
88

99
android {
10+
signingConfigs {
11+
release {
12+
storeFile file('C:\\Users\\suraj\\Downloads\\keystore.jks')
13+
storePassword 'test@123'
14+
keyAlias 'Test'
15+
keyPassword 'test@123'
16+
}
17+
}
1018
compileSdk 35
1119

1220
defaultConfig {
1321
applicationId "com.github.capntrips.kernelflasher"
1422
minSdk 29
1523
targetSdk 34
16-
versionCode 21
17-
versionName "1.0.0-alpha21"
24+
versionCode 25
25+
versionName "1.3.1"
1826

1927
javaCompileOptions {
2028
annotationProcessorOptions {
@@ -33,8 +41,13 @@ android {
3341
}
3442
}
3543
buildTypes {
44+
debug {
45+
debuggable true
46+
signingConfig signingConfigs.release
47+
}
3648
release {
3749
minifyEnabled false
50+
signingConfig signingConfigs.release
3851
}
3952
}
4053
sourceSets {
@@ -91,4 +104,6 @@ dependencies {
91104
implementation(libs.material)
92105
implementation(libs.okhttp)
93106
implementation(libs.kotlinx.serialization.json)
107+
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
108+
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
94109
}

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
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"
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools">
4+
5+
<uses-permission android:name="android.permission.INTERNET" />
6+
7+
<application
8+
android:allowBackup="true"
9+
android:dataExtractionRules="@xml/data_extraction_rules"
10+
android:fullBackupContent="@xml/backup_rules"
11+
android:icon="@mipmap/ic_launcher"
12+
android:label="@string/app_name"
13+
android:roundIcon="@mipmap/ic_launcher_round"
14+
android:supportsRtl="true"
15+
android:theme="@style/Theme.KernelFlasher"
16+
tools:targetApi="33">
17+
<activity
18+
android:name=".MainActivity"
19+
android:exported="true"
20+
android:theme="@style/Theme.MainSplashScreen">
21+
<intent-filter>
22+
<action android:name="android.intent.action.MAIN" />
23+
24+
<category android:name="android.intent.category.LAUNCHER" />
25+
</intent-filter>
26+
</activity>
27+
</application>
28+
29+
</manifest>

app/src/main/assets/flash_ak3.sh

Lines changed: 6 additions & 1 deletion
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;
Lines changed: 111 additions & 0 deletions
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

Lines changed: 93 additions & 2 deletions
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,7 +227,11 @@ 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
@@ -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

Lines changed: 1 addition & 1 deletion
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

0 commit comments

Comments
 (0)