├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── github │ │ └── capntrips │ │ └── kernelflasher │ │ └── IFilesystemService.aidl │ ├── assets │ ├── flash_ak3.sh │ ├── httools_static │ ├── lptools_static │ └── magiskboot │ ├── java │ └── com │ │ └── github │ │ └── capntrips │ │ └── kernelflasher │ │ ├── FilesystemService.kt │ │ ├── MainActivity.kt │ │ ├── MainListener.kt │ │ ├── common │ │ ├── PartitionUtil.kt │ │ ├── extensions │ │ │ ├── ByteArray.kt │ │ │ └── ExtendedFile.kt │ │ └── types │ │ │ ├── backups │ │ │ └── Backup.kt │ │ │ ├── partitions │ │ │ ├── FsMgrFlags.kt │ │ │ ├── FstabEntry.kt │ │ │ └── Partitions.kt │ │ │ └── room │ │ │ ├── AppDatabase.kt │ │ │ ├── Converters.kt │ │ │ └── updates │ │ │ ├── Update.kt │ │ │ └── UpdateDao.kt │ │ └── ui │ │ ├── components │ │ ├── Card.kt │ │ ├── DataCard.kt │ │ ├── DataRow.kt │ │ ├── DataSet.kt │ │ ├── FlashButton.kt │ │ ├── FlashList.kt │ │ ├── SlotCard.kt │ │ └── ViewButton.kt │ │ ├── screens │ │ ├── RefreshableScreen.kt │ │ ├── backups │ │ │ ├── BackupsContent.kt │ │ │ ├── BackupsViewModel.kt │ │ │ └── SlotBackupsContent.kt │ │ ├── error │ │ │ └── ErrorScreen.kt │ │ ├── main │ │ │ ├── MainContent.kt │ │ │ └── MainViewModel.kt │ │ ├── reboot │ │ │ ├── RebootContent.kt │ │ │ └── RebootViewModel.kt │ │ ├── slot │ │ │ ├── SlotContent.kt │ │ │ ├── SlotFlashContent.kt │ │ │ └── SlotViewModel.kt │ │ └── updates │ │ │ ├── UpdatesAddContent.kt │ │ │ ├── UpdatesChangelogContent.kt │ │ │ ├── UpdatesContent.kt │ │ │ ├── UpdatesUrlState.kt │ │ │ ├── UpdatesViewContent.kt │ │ │ └── UpdatesViewModel.kt │ │ └── theme │ │ ├── Color.kt │ │ ├── Theme.kt │ │ └── Type.kt │ └── res │ ├── drawable │ ├── ic_launcher_background.xml │ ├── ic_launcher_foreground.xml │ ├── ic_splash_animation.xml │ └── ic_splash_foreground.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── values-night │ └── themes.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kernel Flasher 2 | 3 | Kernel Flasher is an Android app to flash, backup, and restore kernels. 4 | 5 | ## Usage 6 | 7 | `View` a slot and choose to `Flash` an AK3 zip, `Backup` the kernel related partitions, or `Restore` a previous backup. 8 | 9 | There are also options to toggle the mount and map status of `vendor_dlkm` and to save `dmesg` and `logcat`. 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'org.jetbrains.kotlin.kapt' 5 | id 'org.jetbrains.kotlin.plugin.serialization' 6 | } 7 | 8 | android { 9 | compileSdk 33 10 | 11 | defaultConfig { 12 | applicationId "com.github.capntrips.kernelflasher" 13 | minSdk 30 14 | targetSdk 33 15 | versionCode 14 16 | versionName "1.0.0-alpha14" 17 | 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | } 27 | } 28 | compileOptions { 29 | sourceCompatibility JavaVersion.VERSION_1_8 30 | targetCompatibility JavaVersion.VERSION_1_8 31 | } 32 | kotlinOptions { 33 | jvmTarget = '1.8' 34 | } 35 | buildFeatures { 36 | compose true 37 | } 38 | composeOptions { 39 | kotlinCompilerExtensionVersion compiler_version 40 | } 41 | packagingOptions { 42 | resources { 43 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 44 | } 45 | } 46 | namespace 'com.github.capntrips.kernelflasher' 47 | } 48 | 49 | dependencies { 50 | implementation "androidx.activity:activity-compose:$activity_version" 51 | implementation "androidx.appcompat:appcompat:$appcompat_version" 52 | implementation "androidx.compose.material3:material3:$material3_version" 53 | implementation "androidx.compose.foundation:foundation:$compose_version" 54 | implementation "androidx.compose.ui:ui:$compose_version" 55 | implementation "androidx.core:core-ktx:$core_version" 56 | implementation "androidx.core:core-splashscreen:$splashscreen_version" 57 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" 58 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" 59 | implementation "androidx.navigation:navigation-compose:$nav_version" 60 | implementation "androidx.room:room-runtime:$room_version" 61 | annotationProcessor "androidx.room:room-compiler:$room_version" 62 | kapt "androidx.room:room-compiler:$room_version" 63 | implementation "com.github.topjohnwu.libsu:core:$libsu_version" 64 | implementation "com.github.topjohnwu.libsu:io:$libsu_version" 65 | implementation "com.github.topjohnwu.libsu:nio:$libsu_version" 66 | implementation "com.github.topjohnwu.libsu:service:$libsu_version" 67 | implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version" 68 | implementation "com.google.accompanist:accompanist-swiperefresh:$accompanist_version" 69 | implementation "com.google.android.material:material:$material_version" 70 | implementation("com.squareup.okhttp3:okhttp:$okhttp_version") 71 | implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version" 72 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/aidl/com/github/capntrips/kernelflasher/IFilesystemService.aidl: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher; 2 | 3 | interface IFilesystemService { 4 | IBinder getFileSystemService(); 5 | } -------------------------------------------------------------------------------- /app/src/main/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/assets/httools_static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/KernelFlasher/d1623fe0ba8da17609aa859171ce1a36655625fe/app/src/main/assets/httools_static -------------------------------------------------------------------------------- /app/src/main/assets/lptools_static: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/KernelFlasher/d1623fe0ba8da17609aa859171ce1a36655625fe/app/src/main/assets/lptools_static -------------------------------------------------------------------------------- /app/src/main/assets/magiskboot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/KernelFlasher/d1623fe0ba8da17609aa859171ce1a36655625fe/app/src/main/assets/magiskboot -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/FilesystemService.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher 2 | 3 | import android.content.Intent 4 | import android.os.IBinder 5 | import com.topjohnwu.superuser.ipc.RootService 6 | import com.topjohnwu.superuser.nio.FileSystemManager 7 | 8 | class FilesystemService : RootService() { 9 | inner class FilesystemIPC : IFilesystemService.Stub() { 10 | override fun getFileSystemService(): IBinder { 11 | return FileSystemManager.getService() 12 | } 13 | } 14 | override fun onBind(intent: Intent): IBinder { 15 | return FilesystemIPC() 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.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 androidx.activity.ComponentActivity 15 | import androidx.activity.compose.BackHandler 16 | import androidx.activity.compose.setContent 17 | import androidx.compose.animation.AnimatedVisibilityScope 18 | import androidx.compose.animation.ExperimentalAnimationApi 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.unit.ExperimentalUnitApi 23 | import androidx.core.animation.doOnEnd 24 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 25 | import androidx.core.view.WindowCompat 26 | import androidx.lifecycle.ViewModelProvider 27 | import androidx.lifecycle.viewmodel.compose.viewModel 28 | import androidx.navigation.NavBackStackEntry 29 | import com.github.capntrips.kernelflasher.ui.screens.RefreshableScreen 30 | import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsContent 31 | import com.github.capntrips.kernelflasher.ui.screens.backups.SlotBackupsContent 32 | import com.github.capntrips.kernelflasher.ui.screens.error.ErrorScreen 33 | import com.github.capntrips.kernelflasher.ui.screens.main.MainContent 34 | import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel 35 | import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootContent 36 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotContent 37 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotFlashContent 38 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesAddContent 39 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesChangelogContent 40 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesContent 41 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewContent 42 | import com.github.capntrips.kernelflasher.ui.theme.KernelFlasherTheme 43 | import com.google.accompanist.navigation.animation.AnimatedNavHost 44 | import com.google.accompanist.navigation.animation.composable 45 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController 46 | import com.topjohnwu.superuser.Shell 47 | import com.topjohnwu.superuser.ipc.RootService 48 | import com.topjohnwu.superuser.nio.FileSystemManager 49 | import kotlinx.serialization.ExperimentalSerializationApi 50 | import java.io.File 51 | 52 | 53 | @ExperimentalSerializationApi 54 | @ExperimentalUnitApi 55 | @ExperimentalMaterial3Api 56 | @ExperimentalAnimationApi 57 | class MainActivity : ComponentActivity() { 58 | companion object { 59 | const val TAG: String = "MainActivity" 60 | init { 61 | Shell.setDefaultBuilder(Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER)) 62 | } 63 | } 64 | 65 | private var rootServiceConnected: Boolean = false 66 | private var viewModel: MainViewModel? = null 67 | private lateinit var mainListener: MainListener 68 | var isAwaitingResult = false 69 | 70 | inner class AidlConnection : ServiceConnection { 71 | override fun onServiceConnected(name: ComponentName, service: IBinder) { 72 | if (!rootServiceConnected) { 73 | val ipc: IFilesystemService = IFilesystemService.Stub.asInterface(service) 74 | val binder: IBinder = ipc.fileSystemService 75 | onAidlConnected(FileSystemManager.getRemote(binder)) 76 | rootServiceConnected = true 77 | } 78 | } 79 | 80 | override fun onServiceDisconnected(name: ComponentName) { 81 | setContent { 82 | KernelFlasherTheme { 83 | ErrorScreen(stringResource(R.string.root_service_disconnected)) 84 | } 85 | } 86 | } 87 | } 88 | 89 | private fun copyAsset(filename: String) { 90 | val dest = File(filesDir, filename) 91 | assets.open(filename).use { inputStream -> 92 | dest.outputStream().use { outputStream -> 93 | inputStream.copyTo(outputStream) 94 | } 95 | } 96 | Shell.cmd("chmod +x $dest").exec() 97 | } 98 | 99 | override fun onCreate(savedInstanceState: Bundle?) { 100 | WindowCompat.setDecorFitsSystemWindows(window, false) 101 | val splashScreen = installSplashScreen() 102 | super.onCreate(savedInstanceState) 103 | 104 | splashScreen.setOnExitAnimationListener { splashScreenView -> 105 | val scale = ObjectAnimator.ofPropertyValuesHolder( 106 | splashScreenView.view, 107 | PropertyValuesHolder.ofFloat( 108 | View.SCALE_X, 109 | 1f, 110 | 0f 111 | ), 112 | PropertyValuesHolder.ofFloat( 113 | View.SCALE_Y, 114 | 1f, 115 | 0f 116 | ) 117 | ) 118 | scale.interpolator = AccelerateInterpolator() 119 | scale.duration = 250L 120 | scale.doOnEnd { splashScreenView.remove() } 121 | scale.start() 122 | } 123 | 124 | val content: View = findViewById(android.R.id.content) 125 | content.viewTreeObserver.addOnPreDrawListener( 126 | object : ViewTreeObserver.OnPreDrawListener { 127 | override fun onPreDraw(): Boolean { 128 | return if (viewModel?.isRefreshing == false || Shell.isAppGrantedRoot() == false) { 129 | content.viewTreeObserver.removeOnPreDrawListener(this) 130 | true 131 | } else { 132 | false 133 | } 134 | } 135 | } 136 | ) 137 | 138 | Shell.getShell() 139 | if (Shell.isAppGrantedRoot()!!) { 140 | val intent = Intent(this, FilesystemService::class.java) 141 | RootService.bind(intent, AidlConnection()) 142 | } else { 143 | setContent { 144 | KernelFlasherTheme { 145 | ErrorScreen(stringResource(R.string.root_required)) 146 | } 147 | } 148 | } 149 | } 150 | 151 | fun onAidlConnected(fileSystemManager: FileSystemManager) { 152 | try { 153 | Shell.cmd("cd $filesDir").exec() 154 | copyAsset("lptools_static") 155 | copyAsset("httools_static") 156 | copyAsset("magiskboot") // version: Magisk 25.2 stable release 157 | copyAsset("flash_ak3.sh") 158 | } catch (e: Exception) { 159 | Log.e(TAG, e.message, e) 160 | setContent { 161 | KernelFlasherTheme { 162 | ErrorScreen(e.message!!) 163 | } 164 | } 165 | } 166 | setContent { 167 | val navController = rememberAnimatedNavController() 168 | viewModel = viewModel { 169 | val application = checkNotNull(get(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY)) 170 | MainViewModel(application, fileSystemManager, navController) 171 | } 172 | val mainViewModel = viewModel!! 173 | KernelFlasherTheme { 174 | if (!mainViewModel.hasError) { 175 | mainListener = MainListener { 176 | mainViewModel.refresh(this) 177 | } 178 | val slotViewModelA = mainViewModel.slotA 179 | val slotViewModelB = mainViewModel.slotB 180 | val backupsViewModel = mainViewModel.backups 181 | val updatesViewModel = mainViewModel.updates 182 | val rebootViewModel = mainViewModel.reboot 183 | BackHandler(enabled = mainViewModel.isRefreshing, onBack = {}) 184 | val slotFlashContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> 185 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!! 186 | val slotViewModel = if (slotSuffix == "_a") slotViewModelA else slotViewModelB 187 | RefreshableScreen(mainViewModel, navController) { 188 | SlotFlashContent(slotViewModel, slotSuffix, navController) 189 | } 190 | } 191 | val slotBackupsContent: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> 192 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!! 193 | val slotViewModel = if (slotSuffix == "_a") slotViewModelA else slotViewModelB 194 | if (backStackEntry.arguments?.getString("backupId") != null) { 195 | backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") 196 | } else { 197 | backupsViewModel.clearCurrent() 198 | } 199 | RefreshableScreen(mainViewModel, navController) { 200 | SlotBackupsContent(slotViewModel, backupsViewModel, slotSuffix, navController) 201 | } 202 | } 203 | AnimatedNavHost(navController = navController, startDestination = "main") { 204 | composable("main") { 205 | RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { 206 | MainContent(mainViewModel, navController) 207 | } 208 | } 209 | composable("slot{slotSuffix}") { backStackEntry -> 210 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!! 211 | val slotViewModel = if (slotSuffix == "_a") slotViewModelA else slotViewModelB 212 | if (slotViewModel.wasFlashSuccess != null && navController.currentDestination!!.route.equals("slot{slotSuffix}")) { 213 | slotViewModel.clearFlash(this@MainActivity) 214 | } 215 | RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { 216 | SlotContent(slotViewModel, slotSuffix, navController) 217 | } 218 | } 219 | composable("slot{slotSuffix}/flash", content = slotFlashContent) 220 | composable("slot{slotSuffix}/flash/ak3", content = slotFlashContent) 221 | composable("slot{slotSuffix}/flash/image", content = slotFlashContent) 222 | composable("slot{slotSuffix}/flash/image/flash", content = slotFlashContent) 223 | composable("slot{slotSuffix}/backup", content = slotFlashContent) 224 | composable("slot{slotSuffix}/backup/backup", content = slotFlashContent) 225 | composable("slot{slotSuffix}/backups", content = slotBackupsContent) 226 | composable("slot{slotSuffix}/backups/{backupId}", content = slotBackupsContent) 227 | composable("slot{slotSuffix}/backups/{backupId}/restore", content = slotBackupsContent) 228 | composable("slot{slotSuffix}/backups/{backupId}/restore/restore", content = slotBackupsContent) 229 | composable("slot{slotSuffix}/backups/{backupId}/flash/ak3") { backStackEntry -> 230 | val slotSuffix = backStackEntry.arguments?.getString("slotSuffix")!! 231 | val slotViewModel = if (slotSuffix == "_a") slotViewModelA else slotViewModelB 232 | backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") 233 | if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { 234 | RefreshableScreen(mainViewModel, navController) { 235 | SlotFlashContent(slotViewModel, slotSuffix, navController) 236 | } 237 | } 238 | } 239 | composable("backups") { 240 | backupsViewModel.clearCurrent() 241 | RefreshableScreen(mainViewModel, navController) { 242 | BackupsContent(backupsViewModel, navController) 243 | } 244 | } 245 | composable("backups/{backupId}") { backStackEntry -> 246 | backupsViewModel.currentBackup = backStackEntry.arguments?.getString("backupId") 247 | if (backupsViewModel.backups.containsKey(backupsViewModel.currentBackup)) { 248 | RefreshableScreen(mainViewModel, navController) { 249 | BackupsContent(backupsViewModel, navController) 250 | } 251 | } 252 | } 253 | composable("updates") { 254 | updatesViewModel.clearCurrent() 255 | RefreshableScreen(mainViewModel, navController) { 256 | UpdatesContent(updatesViewModel, navController) 257 | } 258 | } 259 | composable("updates/add") { 260 | RefreshableScreen(mainViewModel, navController) { 261 | UpdatesAddContent(updatesViewModel, navController) 262 | } 263 | } 264 | composable("updates/view/{updateId}") { backStackEntry -> 265 | val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt() 266 | val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId } 267 | updatesViewModel.currentUpdate = currentUpdate 268 | if (updatesViewModel.currentUpdate != null) { 269 | // TODO: enable swipe refresh 270 | RefreshableScreen(mainViewModel, navController) { 271 | UpdatesViewContent(updatesViewModel, navController) 272 | } 273 | } 274 | } 275 | composable("updates/view/{updateId}/changelog") { backStackEntry -> 276 | val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt() 277 | val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId } 278 | updatesViewModel.currentUpdate = currentUpdate 279 | if (updatesViewModel.currentUpdate != null) { 280 | RefreshableScreen(mainViewModel, navController) { 281 | UpdatesChangelogContent(updatesViewModel, navController) 282 | } 283 | } 284 | } 285 | composable("reboot") { 286 | RefreshableScreen(mainViewModel, navController) { 287 | RebootContent(rebootViewModel, navController) 288 | } 289 | } 290 | composable("error/{error}") { backStackEntry -> 291 | val error = backStackEntry.arguments?.getString("error") 292 | ErrorScreen(error!!) 293 | } 294 | } 295 | } else { 296 | ErrorScreen(mainViewModel.error) 297 | } 298 | } 299 | } 300 | } 301 | 302 | public override fun onResume() { 303 | super.onResume() 304 | if (this::mainListener.isInitialized) { 305 | if (!isAwaitingResult) { 306 | mainListener.resume() 307 | } 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/MainListener.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher 2 | 3 | internal class MainListener(private val callback: () -> Unit) { 4 | fun resume() { 5 | callback.invoke() 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/PartitionUtil.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common 2 | 3 | import android.content.Context 4 | import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex 5 | import com.github.capntrips.kernelflasher.common.types.partitions.FstabEntry 6 | import com.topjohnwu.superuser.Shell 7 | import com.topjohnwu.superuser.nio.ExtendedFile 8 | import com.topjohnwu.superuser.nio.FileSystemManager 9 | import kotlinx.serialization.decodeFromString 10 | import kotlinx.serialization.json.Json 11 | import java.io.File 12 | import java.security.DigestOutputStream 13 | import java.security.MessageDigest 14 | 15 | object PartitionUtil { 16 | val PartitionNames = listOf( 17 | "boot", 18 | "vbmeta", 19 | "dtbo", 20 | "vendor_boot", 21 | "vendor_kernel_boot", 22 | "vendor_dlkm", 23 | "init_boot", 24 | "recovery" 25 | ) 26 | 27 | val AvailablePartitions = mutableListOf() 28 | 29 | private var fileSystemManager: FileSystemManager? = null 30 | private var bootDevice: 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 | bootDevice = 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(context: Context, partitionName: String, slotSuffix: String): ExtendedFile? { 61 | var blockDevice: ExtendedFile? = null 62 | val fstabEntry = findPartitionFstabEntry(context, partitionName) 63 | if (fstabEntry != null) { 64 | if (fstabEntry.fsMgrFlags?.logical == true) { 65 | if (fstabEntry.logicalPartitionName == "$partitionName$slotSuffix") { 66 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) 67 | } 68 | } else { 69 | blockDevice = fileSystemManager!!.getFile(fstabEntry.blkDevice) 70 | } 71 | } 72 | if (blockDevice == null) { 73 | val siblingDevice = if (bootDevice != null) fileSystemManager!!.getFile(bootDevice!!, partitionName) else null 74 | val physicalDevice = fileSystemManager!!.getFile("/dev/block/by-name/$partitionName$slotSuffix") 75 | val logicalDevice = fileSystemManager!!.getFile("/dev/block/mapper/$partitionName$slotSuffix") 76 | if (siblingDevice?.exists() == true) { 77 | blockDevice = physicalDevice 78 | } else if (physicalDevice.exists()) { 79 | blockDevice = physicalDevice 80 | } else if (logicalDevice.exists()) { 81 | blockDevice = logicalDevice 82 | } 83 | } 84 | return blockDevice 85 | } 86 | 87 | @Suppress("unused") 88 | fun partitionAvb(context: Context, partitionName: String): String { 89 | val httools = File(context.filesDir, "httools_static") 90 | val result = Shell.cmd("$httools avb $partitionName").exec().out 91 | return if (result.isNotEmpty()) result[0] else "" 92 | } 93 | 94 | fun flashBlockDevice(image: ExtendedFile, blockDevice: ExtendedFile, hashAlgorithm: String): String { 95 | val partitionSize = Shell.cmd("wc -c < $blockDevice").exec().out[0].toUInt() 96 | val imageSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() 97 | if (partitionSize < imageSize) { 98 | throw Error("Partition ${blockDevice.name} is smaller than image") 99 | } 100 | if (partitionSize > imageSize) { 101 | Shell.cmd("dd bs=4096 if=/dev/zero of=$blockDevice").exec() 102 | } 103 | val messageDigest = MessageDigest.getInstance(hashAlgorithm) 104 | image.newInputStream().use { inputStream -> 105 | blockDevice.newOutputStream().use { outputStream -> 106 | DigestOutputStream(outputStream, messageDigest).use { digestOutputStream -> 107 | inputStream.copyTo(digestOutputStream) 108 | } 109 | } 110 | } 111 | return messageDigest.digest().toHex() 112 | } 113 | 114 | @Suppress("SameParameterValue") 115 | fun flashLogicalPartition(context: Context, image: ExtendedFile, blockDevice: ExtendedFile, partitionName: String, slotSuffix: String, hashAlgorithm: String, addMessage: (message: String) -> Unit): String { 116 | val sourceFileSize = Shell.cmd("wc -c < $image").exec().out[0].toUInt() 117 | val lptools = File(context.filesDir, "lptools_static") 118 | Shell.cmd("$lptools remove ${partitionName}_kf").exec() 119 | if (Shell.cmd("$lptools create ${partitionName}_kf $sourceFileSize").exec().isSuccess) { 120 | if (Shell.cmd("$lptools unmap ${partitionName}_kf").exec().isSuccess) { 121 | if (Shell.cmd("$lptools map ${partitionName}_kf").exec().isSuccess) { 122 | val temporaryBlockDevice = fileSystemManager!!.getFile("/dev/block/mapper/${partitionName}_kf") 123 | val hash = flashBlockDevice(image, temporaryBlockDevice, hashAlgorithm) 124 | if (Shell.cmd("$lptools replace ${partitionName}_kf $partitionName$slotSuffix").exec().isSuccess) { 125 | return hash 126 | } else { 127 | throw Error("Replacing $partitionName$slotSuffix failed") 128 | } 129 | } else { 130 | throw Error("Remapping ${partitionName}_kf failed") 131 | } 132 | } else { 133 | throw Error("Unmapping ${partitionName}_kf failed") 134 | } 135 | } else { 136 | addMessage.invoke("Creating ${partitionName}_kf failed. Attempting to resize $partitionName$slotSuffix ...") 137 | val httools = File(context.filesDir, "httools_static") 138 | if (Shell.cmd("$httools umount $partitionName").exec().isSuccess) { 139 | val verityBlockDevice = blockDevice.parentFile!!.getChildFile("${partitionName}-verity") 140 | if (verityBlockDevice.exists()) { 141 | if (!Shell.cmd("$lptools unmap ${partitionName}-verity").exec().isSuccess) { 142 | throw Error("Unmapping ${partitionName}-verity failed") 143 | } 144 | } 145 | if (Shell.cmd("$lptools unmap $partitionName$slotSuffix").exec().isSuccess) { 146 | if (Shell.cmd("$lptools resize $partitionName$slotSuffix \$(wc -c < $image)").exec().isSuccess) { 147 | if (Shell.cmd("$lptools map $partitionName$slotSuffix").exec().isSuccess) { 148 | val hash = flashBlockDevice(image, blockDevice, hashAlgorithm) 149 | if (Shell.cmd("$httools mount $partitionName").exec().isSuccess) { 150 | return hash 151 | } else { 152 | throw Error("Mounting $partitionName failed") 153 | } 154 | } else { 155 | throw Error("Remapping $partitionName$slotSuffix failed") 156 | } 157 | } else { 158 | throw Error("Resizing $partitionName$slotSuffix failed") 159 | } 160 | } else { 161 | throw Error("Unmapping $partitionName$slotSuffix failed") 162 | } 163 | } else { 164 | throw Error("Unmounting $partitionName failed") 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ByteArray.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.extensions 2 | 3 | import kotlin.ByteArray 4 | 5 | object ByteArray { 6 | fun ByteArray.toHex(): String = joinToString(separator = "") { "%02x".format(it) } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/extensions/ExtendedFile.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.extensions 2 | 3 | import com.topjohnwu.superuser.nio.ExtendedFile 4 | import java.io.InputStream 5 | import java.io.InputStreamReader 6 | import java.io.OutputStream 7 | import java.nio.charset.Charset 8 | 9 | object ExtendedFile { 10 | private fun ExtendedFile.reader(charset: Charset = Charsets.UTF_8): InputStreamReader = inputStream().reader(charset) 11 | 12 | private fun ExtendedFile.writeBytes(array: kotlin.ByteArray): Unit = outputStream().use { it.write(array) } 13 | 14 | fun ExtendedFile.readText(charset: Charset = Charsets.UTF_8): String = reader(charset).use { it.readText() } 15 | 16 | @Suppress("unused") 17 | fun ExtendedFile.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit = writeBytes(text.toByteArray(charset)) 18 | 19 | @Suppress("MemberVisibilityCanBePrivate") 20 | fun ExtendedFile.inputStream(): InputStream = newInputStream() 21 | 22 | @Suppress("MemberVisibilityCanBePrivate") 23 | fun ExtendedFile.outputStream(): OutputStream = newOutputStream() 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/backups/Backup.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.backups 2 | 3 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Backup( 8 | val name: String, 9 | val type: String, 10 | val kernelVersion: String, 11 | val bootSha1: String? = null, 12 | val filename: String? = null, 13 | val hashes: Partitions? = null, 14 | val hashAlgorithm: String? = null 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FsMgrFlags.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.partitions 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class FsMgrFlags( 7 | val logical: Boolean = false 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/FstabEntry.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.partitions 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class FstabEntry( 7 | val blkDevice: String, 8 | val mountPoint: String, 9 | val fsType: String, 10 | val logicalPartitionName: String? = null, 11 | val avb: Boolean = false, 12 | val vbmetaPartition: String? = null, 13 | val avbKeys: String? = null, 14 | val fsMgrFlags: FsMgrFlags? = null 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/partitions/Partitions.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.partitions 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Partitions( 7 | val boot: String? = null, 8 | val vbmeta: String? = null, 9 | val dtbo: String? = null, 10 | val vendor_boot: String? = null, 11 | val vendor_kernel_boot: String? = null, 12 | val vendor_dlkm: String? = null, 13 | val init_boot: String? = null, 14 | val recovery: String? = null 15 | ) { 16 | companion object { 17 | fun from(sparseMap: Map) = object { 18 | val map = sparseMap.withDefault { null } 19 | val boot by map 20 | val vbmeta by map 21 | val dtbo by map 22 | val vendor_boot by map 23 | val vendor_kernel_boot by map 24 | val vendor_dlkm by map 25 | val init_boot by map 26 | val recovery by map 27 | val partitions = Partitions(boot, vbmeta, dtbo, vendor_boot, vendor_kernel_boot, vendor_dlkm, init_boot, recovery) 28 | }.partitions 29 | } 30 | 31 | fun get(partition: String): String? { 32 | return when (partition) { 33 | "boot" -> boot 34 | "vbmeta" -> vbmeta 35 | "dtbo" -> dtbo 36 | "vendor_boot" -> vendor_boot 37 | "vendor_kernel_boot" -> vendor_kernel_boot 38 | "vendor_dlkm" -> vendor_dlkm 39 | "init_boot" -> init_boot 40 | "recovery" -> recovery 41 | else -> null 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | import androidx.room.TypeConverters 6 | import com.github.capntrips.kernelflasher.common.types.room.updates.Update 7 | import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateDao 8 | 9 | @Database(entities = [Update::class], version = 1) 10 | @TypeConverters(Converters::class) 11 | abstract class AppDatabase : RoomDatabase() { 12 | abstract fun updateDao(): UpdateDao 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/Converters.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room 2 | 3 | import androidx.room.TypeConverter 4 | import java.util.Date 5 | 6 | class Converters { 7 | @TypeConverter 8 | fun fromTimestamp(value: Long?): Date? { 9 | return value?.let { Date(it) } 10 | } 11 | 12 | @TypeConverter 13 | fun dateToTimestamp(date: Date?): Long? { 14 | return date?.time 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/Update.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room.updates 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.Transient 9 | import kotlinx.serialization.descriptors.PrimitiveKind 10 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 11 | import kotlinx.serialization.encoding.Decoder 12 | import kotlinx.serialization.encoding.Encoder 13 | import kotlinx.serialization.json.JsonElement 14 | import kotlinx.serialization.json.JsonObject 15 | import kotlinx.serialization.json.JsonTransformingSerializer 16 | import kotlinx.serialization.json.buildJsonObject 17 | import java.text.SimpleDateFormat 18 | import java.util.Date 19 | import java.util.Locale 20 | 21 | object DateSerializer : KSerializer { 22 | override val descriptor = PrimitiveSerialDescriptor("Date", PrimitiveKind.STRING) 23 | val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.US) 24 | override fun serialize(encoder: Encoder, value: Date) = encoder.encodeString(formatter.format(value)) 25 | override fun deserialize(decoder: Decoder): Date = formatter.parse(decoder.decodeString())!! 26 | } 27 | 28 | object UpdateSerializer : JsonTransformingSerializer(Update.serializer()) { 29 | override fun transformSerialize(element: JsonElement): JsonElement { 30 | require(element is JsonObject) 31 | return buildJsonObject { 32 | put("kernel", buildJsonObject { 33 | put("name", element["kernelName"]!!) 34 | put("version", element["kernelVersion"]!!) 35 | put("link", element["kernelLink"]!!) 36 | put("changelog_url", element["kernelChangelogUrl"]!!) 37 | put("date", element["kernelDate"]!!) 38 | put("sha1", element["kernelSha1"]!!) 39 | }) 40 | if (element["supportLink"] != null) { 41 | put("support", buildJsonObject { 42 | put("link", element["supportLink"]!!) 43 | }) 44 | } 45 | } 46 | } 47 | override fun transformDeserialize(element: JsonElement): JsonElement { 48 | require(element is JsonObject) 49 | val kernel = element["kernel"] 50 | val support = element["support"] 51 | require(kernel is JsonObject) 52 | require(support is JsonObject?) 53 | return buildJsonObject { 54 | put("kernelName", kernel["name"]!!) 55 | put("kernelVersion", kernel["version"]!!) 56 | put("kernelLink", kernel["link"]!!) 57 | put("kernelChangelogUrl", kernel["changelog_url"]!!) 58 | put("kernelDate", kernel["date"]!!) 59 | put("kernelSha1", kernel["sha1"]!!) 60 | if (support != null && support["link"] != null) { 61 | put("supportLink", support["link"]!!) 62 | } 63 | } 64 | } 65 | } 66 | 67 | @Entity 68 | @Serializable 69 | data class Update( 70 | @PrimaryKey 71 | @Transient 72 | val id: Int? = null, 73 | @ColumnInfo(name = "update_uri") 74 | @Transient 75 | var updateUri: String? = null, 76 | @ColumnInfo(name = "kernel_name") 77 | var kernelName: String, 78 | @ColumnInfo(name = "kernel_version") 79 | var kernelVersion: String, 80 | @ColumnInfo(name = "kernel_link") 81 | var kernelLink: String, 82 | @ColumnInfo(name = "kernel_changelog_url") 83 | var kernelChangelogUrl: String, 84 | @ColumnInfo(name = "kernel_date") 85 | @Serializable(DateSerializer::class) 86 | var kernelDate: Date, 87 | @ColumnInfo(name = "kernel_sha1") 88 | var kernelSha1: String, 89 | @ColumnInfo(name = "support_link") 90 | var supportLink: String?, 91 | @ColumnInfo(name = "last_updated") 92 | @Transient 93 | var lastUpdated: Date? = null, 94 | ) 95 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/common/types/room/updates/UpdateDao.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.common.types.room.updates 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Delete 5 | import androidx.room.Insert 6 | import androidx.room.Query 7 | 8 | @Dao 9 | interface UpdateDao { 10 | @Query("""SELECT * FROM "update"""") 11 | fun getAll(): List 12 | 13 | @Query("""SELECT * FROM "update" WHERE id IN (:id)""") 14 | fun load(id: Int): Update 15 | 16 | @Insert 17 | fun insert(update: Update): Long 18 | 19 | @androidx.room.Update 20 | fun update(update: Update) 21 | 22 | @Delete 23 | fun delete(update: Update) 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.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/capntrips/kernelflasher/ui/components/DataCard.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.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.padding(0.dp, 9.dp, 8.dp, 9.dp).weight(1.0f), 33 | text = title, 34 | color = MaterialTheme.colorScheme.primary, 35 | style = MaterialTheme.typography.titleLarge 36 | ) 37 | if (button != null) { 38 | button() 39 | } 40 | } 41 | if (content != null) { 42 | Spacer(Modifier.height(10.dp)) 43 | content() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataRow.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.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/java/com/github/capntrips/kernelflasher/ui/components/DataSet.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.text.TextStyle 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun DataSet( 16 | label: String, 17 | labelColor: Color = Color.Unspecified, 18 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium, 19 | content: @Composable (ColumnScope.() -> Unit) 20 | ) { 21 | Text( 22 | text = label, 23 | color = labelColor, 24 | style = labelStyle 25 | ) 26 | Column(Modifier.padding(start = 16.dp)) { 27 | content() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import android.net.Uri 4 | import 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.material3.ExperimentalMaterial3Api 10 | import androidx.compose.material3.OutlinedButton 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.unit.ExperimentalUnitApi 18 | import androidx.compose.ui.unit.dp 19 | import com.github.capntrips.kernelflasher.MainActivity 20 | 21 | @ExperimentalUnitApi 22 | @ExperimentalAnimationApi 23 | @ExperimentalMaterial3Api 24 | @Composable 25 | fun FlashButton( 26 | buttonText: String, 27 | callback: (uri: Uri) -> Unit 28 | ) { 29 | val mainActivity = LocalContext.current as MainActivity 30 | val result = remember { mutableStateOf(null) } 31 | val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { 32 | result.value = it 33 | if (it == null) { 34 | mainActivity.isAwaitingResult = false 35 | } 36 | } 37 | OutlinedButton( 38 | modifier = Modifier 39 | .fillMaxWidth(), 40 | shape = RoundedCornerShape(4.dp), 41 | onClick = { 42 | mainActivity.isAwaitingResult = true 43 | launcher.launch("*/*") 44 | } 45 | ) { 46 | Text(buttonText) 47 | } 48 | result.value?.let {uri -> 49 | if (mainActivity.isAwaitingResult) { 50 | callback.invoke(uri) 51 | } 52 | mainActivity.isAwaitingResult = false 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.interaction.collectIsDraggedAsState 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.foundation.lazy.LazyListState 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.foundation.lazy.rememberLazyListState 14 | import androidx.compose.material3.LocalTextStyle 15 | import androidx.compose.material3.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.LaunchedEffect 18 | import androidx.compose.runtime.getValue 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.runtime.setValue 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.composed 24 | import androidx.compose.ui.draw.drawWithContent 25 | import androidx.compose.ui.geometry.CornerRadius 26 | import androidx.compose.ui.geometry.Offset 27 | import androidx.compose.ui.geometry.Size 28 | import androidx.compose.ui.graphics.Color 29 | import androidx.compose.ui.text.font.FontFamily 30 | import androidx.compose.ui.unit.Dp 31 | import androidx.compose.ui.unit.ExperimentalUnitApi 32 | import androidx.compose.ui.unit.TextUnit 33 | import androidx.compose.ui.unit.TextUnitType 34 | import androidx.compose.ui.unit.dp 35 | 36 | @ExperimentalUnitApi 37 | @Composable 38 | fun ColumnScope.FlashList( 39 | cardTitle: String, 40 | output: List, 41 | content: @Composable ColumnScope.() -> Unit 42 | ) { 43 | val listState = rememberLazyListState() 44 | var hasDragged by remember { mutableStateOf(false) } 45 | val isDragged by listState.interactionSource.collectIsDraggedAsState() 46 | if (isDragged) { 47 | hasDragged = true 48 | } 49 | var shouldScroll = false 50 | if (!hasDragged) { 51 | if (listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index != null) { 52 | if (listState.layoutInfo.totalItemsCount - listState.layoutInfo.visibleItemsInfo.size > listState.layoutInfo.visibleItemsInfo.firstOrNull()?.index!!) { 53 | shouldScroll = true 54 | } 55 | } 56 | } 57 | LaunchedEffect(shouldScroll) { 58 | listState.animateScrollToItem(output.size) 59 | } 60 | DataCard (cardTitle) 61 | Spacer(Modifier.height(4.dp)) 62 | LazyColumn( 63 | Modifier 64 | .weight(1.0f) 65 | .fillMaxSize() 66 | .scrollbar(listState), 67 | listState 68 | ) { 69 | items(output) { message -> 70 | Text(message, 71 | style = LocalTextStyle.current.copy( 72 | fontFamily = FontFamily.Monospace, 73 | fontSize = TextUnit(12.0f, TextUnitType.Sp), 74 | lineHeight = TextUnit(18.0f, TextUnitType.Sp) 75 | ) 76 | ) 77 | } 78 | } 79 | content() 80 | } 81 | 82 | // https://stackoverflow.com/a/68056586/434343 83 | fun Modifier.scrollbar( 84 | state: LazyListState, 85 | width: Dp = 6.dp 86 | ): Modifier = composed { 87 | var visibleItemsCountChanged = false 88 | var visibleItemsCount by remember { mutableStateOf(state.layoutInfo.visibleItemsInfo.size) } 89 | if (visibleItemsCount != state.layoutInfo.visibleItemsInfo.size) { 90 | visibleItemsCountChanged = true 91 | @Suppress("UNUSED_VALUE") 92 | visibleItemsCount = state.layoutInfo.visibleItemsInfo.size 93 | } 94 | 95 | val hidden = state.layoutInfo.visibleItemsInfo.size == state.layoutInfo.totalItemsCount 96 | val targetAlpha = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0.5f else 0f 97 | val delay = if (!hidden && (state.isScrollInProgress || visibleItemsCountChanged)) 0 else 250 98 | val duration = if (hidden || visibleItemsCountChanged) 0 else if (state.isScrollInProgress) 150 else 500 99 | 100 | val alpha by animateFloatAsState( 101 | targetValue = targetAlpha, 102 | animationSpec = tween(delayMillis = delay, durationMillis = duration) 103 | ) 104 | 105 | drawWithContent { 106 | drawContent() 107 | 108 | val firstVisibleElementIndex = state.layoutInfo.visibleItemsInfo.firstOrNull()?.index 109 | val needDrawScrollbar = state.isScrollInProgress || visibleItemsCountChanged || alpha > 0.0f 110 | 111 | if (needDrawScrollbar && firstVisibleElementIndex != null) { 112 | val elementHeight = this.size.height / state.layoutInfo.totalItemsCount 113 | val scrollbarOffsetY = firstVisibleElementIndex * elementHeight 114 | val scrollbarHeight = state.layoutInfo.visibleItemsInfo.size * elementHeight 115 | 116 | drawRoundRect( 117 | color = Color.Gray, 118 | topLeft = Offset(this.size.width - width.toPx(), scrollbarOffsetY), 119 | size = Size(width.toPx(), scrollbarHeight), 120 | cornerRadius = CornerRadius(width.toPx(), width.toPx()), 121 | alpha = alpha 122 | ) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.mutableStateOf 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.capntrips.kernelflasher.R 14 | import com.github.capntrips.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 { mutableStateOf(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("%s, %s", stringResource(R.string.exists), stringResource(R.string.mounted)) 60 | } else { 61 | String.format("%s, %s", stringResource(R.string.exists), stringResource(R.string.unmounted)) 62 | } 63 | } 64 | DataRow(stringResource(R.string.vendor_dlkm), vendorDlkmValue, mutableMaxWidth = cardWidth) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.components 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.shape.RoundedCornerShape 6 | import androidx.compose.material3.ButtonDefaults 7 | import androidx.compose.material3.Text 8 | import androidx.compose.material3.TextButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.unit.LayoutDirection 13 | import androidx.compose.ui.unit.dp 14 | import com.github.capntrips.kernelflasher.R 15 | 16 | @Composable 17 | fun ViewButton( 18 | onClick: () -> Unit 19 | ) { 20 | TextButton( 21 | modifier = Modifier.padding(0.dp), 22 | shape = RoundedCornerShape(4.0.dp), 23 | contentPadding = PaddingValues( 24 | horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, 25 | vertical = ButtonDefaults.ContentPadding.calculateTopPadding() 26 | ), 27 | onClick = onClick 28 | ) { 29 | Text(stringResource(R.string.view), maxLines = 1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.fadeIn 5 | import androidx.compose.animation.fadeOut 6 | import androidx.compose.foundation.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.icons.Icons 21 | import androidx.compose.material.icons.filled.ArrowBack 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.Icon 24 | import androidx.compose.material3.IconButton 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Scaffold 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.ui.Alignment 30 | import androidx.compose.ui.Modifier 31 | import androidx.compose.ui.platform.LocalContext 32 | import androidx.compose.ui.res.stringResource 33 | import androidx.compose.ui.unit.dp 34 | import androidx.navigation.NavController 35 | import com.github.capntrips.kernelflasher.R 36 | import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel 37 | import com.google.accompanist.swiperefresh.SwipeRefresh 38 | import com.google.accompanist.swiperefresh.SwipeRefreshIndicator 39 | import com.google.accompanist.swiperefresh.rememberSwipeRefreshState 40 | 41 | @ExperimentalMaterial3Api 42 | @Composable 43 | fun RefreshableScreen( 44 | viewModel: MainViewModel, 45 | navController: NavController, 46 | swipeEnabled: Boolean = false, 47 | content: @Composable ColumnScope.() -> Unit 48 | ) { 49 | val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues() 50 | val navigationBars = WindowInsets.navigationBars.asPaddingValues() 51 | Scaffold( 52 | topBar = { 53 | Box( 54 | Modifier 55 | .fillMaxWidth() 56 | .padding(statusBar)) { 57 | if (navController.previousBackStackEntry != null) { 58 | AnimatedVisibility( 59 | !viewModel.isRefreshing, 60 | enter = fadeIn(), 61 | exit = fadeOut() 62 | ) { 63 | IconButton( 64 | onClick = { navController.popBackStack() }, 65 | modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp) 66 | ) { 67 | Icon( 68 | Icons.Filled.ArrowBack, 69 | contentDescription = stringResource(R.string.back), 70 | tint = MaterialTheme.colorScheme.onSurface 71 | ) 72 | } 73 | } 74 | } 75 | Box( 76 | Modifier 77 | .fillMaxWidth() 78 | .padding(16.dp)) { 79 | Text( 80 | modifier = Modifier.align(Alignment.Center), 81 | text = stringResource(R.string.app_name), 82 | style = MaterialTheme.typography.headlineSmall 83 | ) 84 | } 85 | } 86 | } 87 | ) { paddingValues -> 88 | val context = LocalContext.current 89 | SwipeRefresh( 90 | modifier = Modifier 91 | .padding(paddingValues) 92 | .fillMaxSize(), 93 | state = rememberSwipeRefreshState(viewModel.isRefreshing), 94 | swipeEnabled = swipeEnabled, 95 | // TODO: move onRefresh to signature? 96 | onRefresh = { viewModel.refresh(context) }, 97 | indicator = { state, trigger -> 98 | SwipeRefreshIndicator( 99 | state = state, 100 | refreshTriggerDistance = trigger, 101 | backgroundColor = MaterialTheme.colorScheme.background, 102 | contentColor = MaterialTheme.colorScheme.primaryContainer, 103 | scale = true 104 | ) 105 | } 106 | ) { 107 | Column( 108 | modifier = Modifier 109 | .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding()) 110 | .fillMaxSize() 111 | .verticalScroll(rememberScrollState()), 112 | content = content 113 | ) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.backups 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.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.mutableStateOf 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.capntrips.kernelflasher.R 27 | import com.github.capntrips.kernelflasher.common.PartitionUtil 28 | import com.github.capntrips.kernelflasher.ui.components.DataCard 29 | import com.github.capntrips.kernelflasher.ui.components.DataRow 30 | import com.github.capntrips.kernelflasher.ui.components.DataSet 31 | import com.github.capntrips.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 { mutableStateOf(0) } 43 | val currentBackup = viewModel.backups.getValue(viewModel.currentBackup!!) 44 | DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth) 45 | DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 46 | if (currentBackup.type == "raw") { 47 | DataRow( 48 | label = stringResource(R.string.boot_sha1), 49 | value = currentBackup.bootSha1!!.substring(0, 8), 50 | valueStyle = MaterialTheme.typography.titleSmall.copy( 51 | fontFamily = FontFamily.Monospace, 52 | fontWeight = FontWeight.Medium 53 | ), 54 | mutableMaxWidth = cardWidth 55 | ) 56 | if (currentBackup.hashes != null) { 57 | val hashWidth = remember { mutableStateOf(0) } 58 | DataSet(stringResource(R.string.hashes)) { 59 | for (partitionName in PartitionUtil.PartitionNames) { 60 | val hash = currentBackup.hashes.get(partitionName) 61 | if (hash != null) { 62 | DataRow( 63 | label = partitionName, 64 | value = hash.substring(0, 8), 65 | valueStyle = MaterialTheme.typography.titleSmall.copy( 66 | fontFamily = FontFamily.Monospace, 67 | fontWeight = FontWeight.Medium 68 | ), 69 | mutableMaxWidth = hashWidth 70 | ) 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | AnimatedVisibility(!viewModel.isRefreshing) { 78 | Column { 79 | Spacer(Modifier.height(5.dp)) 80 | OutlinedButton( 81 | modifier = Modifier 82 | .fillMaxWidth(), 83 | shape = RoundedCornerShape(4.dp), 84 | onClick = { viewModel.delete(context) { navController.popBackStack() } } 85 | ) { 86 | Text(stringResource(R.string.delete)) 87 | } 88 | } 89 | } 90 | } else { 91 | DataCard(stringResource(R.string.backups)) 92 | AnimatedVisibility(viewModel.needsMigration) { 93 | Column { 94 | Spacer(Modifier.height(5.dp)) 95 | OutlinedButton( 96 | modifier = Modifier 97 | .fillMaxWidth(), 98 | shape = RoundedCornerShape(4.dp), 99 | onClick = { viewModel.migrate(context) } 100 | ) { 101 | Text(stringResource(R.string.migrate)) 102 | } 103 | } 104 | } 105 | if (viewModel.backups.isNotEmpty()) { 106 | for (id in viewModel.backups.keys.sortedByDescending { it }) { 107 | val currentBackup = viewModel.backups[id]!! 108 | Spacer(Modifier.height(16.dp)) 109 | DataCard( 110 | title = id, 111 | button = { 112 | AnimatedVisibility(!viewModel.isRefreshing) { 113 | Column { 114 | ViewButton(onClick = { 115 | navController.navigate("backups/$id") 116 | }) 117 | } 118 | } 119 | } 120 | ) { 121 | val cardWidth = remember { mutableStateOf(0) } 122 | if (currentBackup.type == "raw") { 123 | DataRow( 124 | label = stringResource(R.string.boot_sha1), 125 | value = currentBackup.bootSha1!!.substring(0, 8), 126 | valueStyle = MaterialTheme.typography.titleSmall.copy( 127 | fontFamily = FontFamily.Monospace, 128 | fontWeight = FontWeight.Medium 129 | ), 130 | mutableMaxWidth = cardWidth 131 | ) 132 | } 133 | DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 134 | } 135 | } 136 | } else { 137 | Spacer(Modifier.height(32.dp)) 138 | Text( 139 | stringResource(R.string.no_backups_found), 140 | modifier = Modifier.fillMaxWidth(), 141 | textAlign = TextAlign.Center, 142 | fontStyle = FontStyle.Italic 143 | ) 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.backups 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.compose.runtime.MutableState 8 | import androidx.compose.runtime.mutableStateListOf 9 | import androidx.compose.runtime.mutableStateMapOf 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.snapshots.SnapshotStateList 12 | import androidx.compose.runtime.snapshots.SnapshotStateMap 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.viewModelScope 15 | import androidx.navigation.NavController 16 | import com.github.capntrips.kernelflasher.common.PartitionUtil 17 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream 18 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.readText 19 | import com.github.capntrips.kernelflasher.common.types.backups.Backup 20 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions 21 | import com.topjohnwu.superuser.Shell 22 | import com.topjohnwu.superuser.nio.ExtendedFile 23 | import com.topjohnwu.superuser.nio.FileSystemManager 24 | import kotlinx.coroutines.Dispatchers 25 | import kotlinx.coroutines.launch 26 | import kotlinx.coroutines.withContext 27 | import kotlinx.serialization.decodeFromString 28 | import kotlinx.serialization.encodeToString 29 | import kotlinx.serialization.json.Json 30 | import java.io.File 31 | import java.io.FileInputStream 32 | import java.time.LocalDateTime 33 | import java.time.format.DateTimeFormatter 34 | import java.util.Properties 35 | 36 | class BackupsViewModel( 37 | context: Context, 38 | private val fileSystemManager: FileSystemManager, 39 | private val navController: NavController, 40 | private val _isRefreshing: MutableState, 41 | private val _backups: MutableMap 42 | ) : ViewModel() { 43 | companion object { 44 | const val TAG: String = "KernelFlasher/BackupsState" 45 | } 46 | 47 | private val _restoreOutput: SnapshotStateList = mutableStateListOf() 48 | var currentBackup: String? = null 49 | set(value) { 50 | if (value != field) { 51 | if (_backups[value]?.hashes != null) { 52 | PartitionUtil.AvailablePartitions.forEach { partitionName -> 53 | if (_backups[value]!!.hashes!!.get(partitionName) != null) { 54 | _backupPartitions[partitionName] = true 55 | } 56 | } 57 | } 58 | field = value 59 | } 60 | } 61 | var wasRestored: Boolean? = null 62 | private val _backupPartitions: SnapshotStateMap = mutableStateMapOf() 63 | private val hashAlgorithm: String = "SHA-256" 64 | @Suppress("PropertyName") 65 | @Deprecated("Backup migration will be removed in the first stable release") 66 | private var _needsMigration: MutableState = mutableStateOf(false) 67 | 68 | val restoreOutput: List 69 | get() = _restoreOutput 70 | val backupPartitions: MutableMap 71 | get() = _backupPartitions 72 | val isRefreshing: Boolean 73 | get() = _isRefreshing.value 74 | val backups: Map 75 | get() = _backups 76 | @Suppress("DeprecatedCallableAddReplaceWith") 77 | @Deprecated("Backup migration will be removed in the first stable release") 78 | val needsMigration: Boolean 79 | get() = _needsMigration.value 80 | 81 | init { 82 | refresh(context) 83 | } 84 | 85 | fun refresh(context: Context) { 86 | val oldDir = context.getExternalFilesDir(null) 87 | val oldBackupsDir = File(oldDir, "backups") 88 | @Deprecated("Backup migration will be removed in the first stable release") 89 | _needsMigration.value = oldBackupsDir.exists() && oldBackupsDir.listFiles()?.size!! > 0 90 | @SuppressLint("SdCardPath") 91 | val externalDir = File("/sdcard/KernelFlasher") 92 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 93 | if (backupsDir.exists()) { 94 | val children = backupsDir.listFiles() 95 | if (children != null) { 96 | for (child in children.sortedByDescending{it.name}) { 97 | if (!child.isDirectory) { 98 | continue 99 | } 100 | val jsonFile = child.getChildFile("backup.json") 101 | if (jsonFile.exists()) { 102 | _backups[child.name] = Json.decodeFromString(jsonFile.readText()) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | private fun launch(block: suspend () -> Unit) { 110 | viewModelScope.launch(Dispatchers.IO) { 111 | _isRefreshing.value = true 112 | try { 113 | block() 114 | } catch (e: Exception) { 115 | withContext (Dispatchers.Main) { 116 | Log.e(TAG, e.message, e) 117 | navController.navigate("error/${e.message}") { 118 | popUpTo("main") 119 | } 120 | } 121 | } 122 | _isRefreshing.value = false 123 | } 124 | } 125 | 126 | @Suppress("SameParameterValue") 127 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 128 | Log.d(TAG, message) 129 | if (!shouldThrow) { 130 | viewModelScope.launch(Dispatchers.Main) { 131 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 132 | } 133 | } else { 134 | throw Exception(message) 135 | } 136 | } 137 | 138 | fun clearCurrent() { 139 | currentBackup = null 140 | clearRestore() 141 | } 142 | 143 | private fun addMessage(message: String) { 144 | viewModelScope.launch(Dispatchers.Main) { 145 | _restoreOutput.add(message) 146 | } 147 | } 148 | 149 | @Suppress("FunctionName") 150 | private fun _clearRestore() { 151 | _restoreOutput.clear() 152 | wasRestored = null 153 | } 154 | 155 | private fun clearRestore() { 156 | _clearRestore() 157 | _backupPartitions.clear() 158 | } 159 | 160 | @Suppress("unused") 161 | @SuppressLint("SdCardPath") 162 | fun saveLog(context: Context) { 163 | launch { 164 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 165 | val log = File("/sdcard/Download/restore-log--$now.log") 166 | log.writeText(restoreOutput.joinToString("\n")) 167 | if (log.exists()) { 168 | log(context, "Saved restore log to $log") 169 | } else { 170 | log(context, "Failed to save $log", shouldThrow = true) 171 | } 172 | } 173 | } 174 | 175 | private fun restorePartitions(context: Context, source: ExtendedFile, slotSuffix: String): Partitions? { 176 | val partitions = HashMap() 177 | for (partitionName in PartitionUtil.PartitionNames) { 178 | if (_backups[currentBackup]?.hashes == null || _backupPartitions[partitionName] == true) { 179 | val image = source.getChildFile("$partitionName.img") 180 | if (image.exists()) { 181 | val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) 182 | if (blockDevice != null && blockDevice.exists()) { 183 | addMessage("Restoring $partitionName") 184 | partitions[partitionName] = if (PartitionUtil.isPartitionLogical(context, partitionName)) { 185 | PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message -> 186 | addMessage(message) 187 | } 188 | } else { 189 | PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) 190 | } 191 | } else { 192 | log(context, "Partition $partitionName was not found", shouldThrow = true) 193 | } 194 | } 195 | } 196 | } 197 | if (partitions.isNotEmpty()) { 198 | return Partitions.from(partitions) 199 | } 200 | return null 201 | } 202 | 203 | fun restore(context: Context, slotSuffix: String) { 204 | launch { 205 | _clearRestore() 206 | @SuppressLint("SdCardPath") 207 | val externalDir = File("/sdcard/KernelFlasher") 208 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 209 | val backupDir = backupsDir.getChildFile(currentBackup!!) 210 | if (!backupDir.exists()) { 211 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 212 | return@launch 213 | } 214 | addMessage("Restoring backup $currentBackup") 215 | val hashes = restorePartitions(context, backupDir, slotSuffix) 216 | if (hashes == null) { 217 | log(context, "No partitions restored", shouldThrow = true) 218 | } 219 | addMessage("Backup $currentBackup restored") 220 | wasRestored = true 221 | } 222 | } 223 | 224 | fun delete(context: Context, callback: () -> Unit) { 225 | launch { 226 | @SuppressLint("SdCardPath") 227 | val externalDir = File("/sdcard/KernelFlasher") 228 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 229 | val backupDir = backupsDir.getChildFile(currentBackup!!) 230 | if (!backupDir.exists()) { 231 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 232 | return@launch 233 | } 234 | backupDir.deleteRecursively() 235 | _backups.remove(currentBackup!!) 236 | withContext(Dispatchers.Main) { 237 | callback.invoke() 238 | } 239 | } 240 | } 241 | 242 | @SuppressLint("SdCardPath") 243 | @Deprecated("Backup migration will be removed in the first stable release") 244 | fun migrate(context: Context) { 245 | launch { 246 | val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher") 247 | if (!externalDir.exists()) { 248 | if (!externalDir.mkdir()) { 249 | log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true) 250 | } 251 | } 252 | val backupsDir = externalDir.getChildFile("backups") 253 | if (!backupsDir.exists()) { 254 | if (!backupsDir.mkdir()) { 255 | log(context, "Failed to create backups dir", shouldThrow = true) 256 | } 257 | } 258 | val oldDir = context.getExternalFilesDir(null) 259 | val oldBackupsDir = File(oldDir, "backups") 260 | if (oldBackupsDir.exists()) { 261 | val indentedJson = Json { prettyPrint = true } 262 | val children = oldBackupsDir.listFiles() 263 | if (children != null) { 264 | for (child in children.sortedByDescending{it.name}) { 265 | if (!child.isDirectory) { 266 | child.delete() 267 | continue 268 | } 269 | val propFile = File(child, "backup.prop") 270 | @Suppress("BlockingMethodInNonBlockingContext") 271 | val inputStream = FileInputStream(propFile) 272 | val props = Properties() 273 | @Suppress("BlockingMethodInNonBlockingContext") 274 | props.load(inputStream) 275 | 276 | val name = child.name 277 | val type = props.getProperty("type", "raw") 278 | val kernelVersion = props.getProperty("kernel") 279 | val bootSha1 = if (type == "raw") props.getProperty("sha1") else null 280 | val filename = if (type == "ak3") "ak3.zip" else null 281 | propFile.delete() 282 | 283 | val dest = backupsDir.getChildFile(child.name) 284 | @Suppress("BlockingMethodInNonBlockingContext") 285 | Shell.cmd("mv $child $dest").exec() 286 | if (!dest.exists()) { 287 | throw Error("Too slow") 288 | } 289 | val jsonFile = dest.getChildFile("backup.json") 290 | val backup = Backup(name, type, kernelVersion, bootSha1, filename) 291 | @Suppress("BlockingMethodInNonBlockingContext") 292 | jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } 293 | _backups[name] = backup 294 | } 295 | } 296 | oldBackupsDir.delete() 297 | } 298 | refresh(context) 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.backups 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.offset 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material3.ButtonDefaults 13 | import androidx.compose.material3.Checkbox 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.OutlinedButton 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.mutableStateOf 20 | import androidx.compose.runtime.remember 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.alpha 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.platform.LocalContext 26 | import androidx.compose.ui.res.stringResource 27 | import androidx.compose.ui.text.font.FontFamily 28 | import androidx.compose.ui.text.font.FontStyle 29 | import androidx.compose.ui.text.font.FontWeight 30 | import androidx.compose.ui.text.style.TextAlign 31 | import androidx.compose.ui.unit.ExperimentalUnitApi 32 | import androidx.compose.ui.unit.dp 33 | import androidx.navigation.NavController 34 | import com.github.capntrips.kernelflasher.R 35 | import com.github.capntrips.kernelflasher.common.PartitionUtil 36 | import com.github.capntrips.kernelflasher.ui.components.DataCard 37 | import com.github.capntrips.kernelflasher.ui.components.DataRow 38 | import com.github.capntrips.kernelflasher.ui.components.DataSet 39 | import com.github.capntrips.kernelflasher.ui.components.FlashList 40 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 41 | import com.github.capntrips.kernelflasher.ui.components.ViewButton 42 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel 43 | 44 | @ExperimentalUnitApi 45 | @ExperimentalMaterial3Api 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(backupsViewModel.currentBackup)) { 64 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) 65 | DataCard(backupsViewModel.currentBackup!!) { 66 | val cardWidth = remember { mutableStateOf(0) } 67 | DataRow(stringResource(R.string.backup_type), currentBackup.type, mutableMaxWidth = cardWidth) 68 | DataRow(stringResource(R.string.kernel_version), currentBackup.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 69 | if (currentBackup.type == "raw") { 70 | DataRow( 71 | label = stringResource(R.string.boot_sha1), 72 | value = currentBackup.bootSha1!!.substring(0, 8), 73 | valueStyle = MaterialTheme.typography.titleSmall.copy( 74 | fontFamily = FontFamily.Monospace, 75 | fontWeight = FontWeight.Medium 76 | ), 77 | mutableMaxWidth = cardWidth 78 | ) 79 | if (currentBackup.hashes != null) { 80 | val hashWidth = remember { mutableStateOf(0) } 81 | DataSet(stringResource(R.string.hashes)) { 82 | for (partitionName in PartitionUtil.PartitionNames) { 83 | val hash = currentBackup.hashes.get(partitionName) 84 | if (hash != null) { 85 | DataRow( 86 | label = partitionName, 87 | value = hash.substring(0, 8), 88 | valueStyle = MaterialTheme.typography.titleSmall.copy( 89 | fontFamily = FontFamily.Monospace, 90 | fontWeight = FontWeight.Medium 91 | ), 92 | mutableMaxWidth = hashWidth 93 | ) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | } 100 | AnimatedVisibility(!slotViewModel.isRefreshing) { 101 | Column { 102 | Spacer(Modifier.height(5.dp)) 103 | if (slotViewModel.isActive) { 104 | if (currentBackup.type == "raw") { 105 | OutlinedButton( 106 | modifier = Modifier 107 | .fillMaxWidth(), 108 | shape = RoundedCornerShape(4.dp), 109 | onClick = { 110 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore") 111 | } 112 | ) { 113 | Text(stringResource(R.string.restore)) 114 | } 115 | } else if (currentBackup.type == "ak3") { 116 | OutlinedButton( 117 | modifier = Modifier 118 | .fillMaxWidth(), 119 | shape = RoundedCornerShape(4.dp), 120 | onClick = { 121 | slotViewModel.flashAk3(context, backupsViewModel.currentBackup!!, currentBackup.filename!!) 122 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { 123 | popUpTo("slot{slotSuffix}") 124 | } 125 | } 126 | ) { 127 | Text(stringResource(R.string.flash)) 128 | } 129 | } 130 | } 131 | OutlinedButton( 132 | modifier = Modifier 133 | .fillMaxWidth(), 134 | shape = RoundedCornerShape(4.dp), 135 | onClick = { backupsViewModel.delete(context) { navController.popBackStack() } } 136 | ) { 137 | Text(stringResource(R.string.delete)) 138 | } 139 | } 140 | } 141 | } else { 142 | DataCard(stringResource(R.string.backups)) 143 | val backups = backupsViewModel.backups.filter { it.value.bootSha1.equals(slotViewModel.sha1) || it.value.type == "ak3" } 144 | if (backups.isNotEmpty()) { 145 | for (id in backups.keys.sortedByDescending { it }) { 146 | Spacer(Modifier.height(16.dp)) 147 | DataCard( 148 | title = id, 149 | button = { 150 | AnimatedVisibility(!slotViewModel.isRefreshing) { 151 | ViewButton(onClick = { 152 | navController.navigate("slot$slotSuffix/backups/$id") 153 | }) 154 | } 155 | } 156 | ) { 157 | DataRow(stringResource(R.string.kernel_version), backups[id]!!.kernelVersion, clickable = true) 158 | } 159 | } 160 | } else { 161 | Spacer(Modifier.height(32.dp)) 162 | Text( 163 | stringResource(R.string.no_backups_found), 164 | modifier = Modifier.fillMaxWidth(), 165 | textAlign = TextAlign.Center, 166 | fontStyle = FontStyle.Italic 167 | ) 168 | } 169 | } 170 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backups/{backupId}/restore") { 171 | DataCard (stringResource(R.string.restore)) 172 | Spacer(Modifier.height(5.dp)) 173 | val disabledColor = ButtonDefaults.buttonColors( 174 | Color.Transparent, 175 | MaterialTheme.colorScheme.onSurface 176 | ) 177 | val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) 178 | if (currentBackup.hashes != null) { 179 | for (partitionName in PartitionUtil.PartitionNames) { 180 | val hash = currentBackup.hashes.get(partitionName) 181 | if (hash != null) { 182 | OutlinedButton( 183 | modifier = Modifier 184 | .fillMaxWidth() 185 | .alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), 186 | shape = RoundedCornerShape(4.dp), 187 | colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor, 188 | enabled = backupsViewModel.backupPartitions[partitionName] != null, 189 | onClick = { 190 | backupsViewModel.backupPartitions[partitionName] = !backupsViewModel.backupPartitions[partitionName]!! 191 | }, 192 | ) { 193 | Box(Modifier.fillMaxWidth()) { 194 | Checkbox(backupsViewModel.backupPartitions[partitionName] == true, null, 195 | Modifier 196 | .align(Alignment.CenterStart) 197 | .offset(x = -(16.dp))) 198 | Text(partitionName, Modifier.align(Alignment.Center)) 199 | } 200 | } 201 | } 202 | } 203 | } else { 204 | Text( 205 | stringResource(R.string.partition_selection_unavailable), 206 | modifier = Modifier.fillMaxWidth(), 207 | textAlign = TextAlign.Center, 208 | fontStyle = FontStyle.Italic 209 | ) 210 | Spacer(Modifier.height(5.dp)) 211 | } 212 | OutlinedButton( 213 | modifier = Modifier 214 | .fillMaxWidth(), 215 | shape = RoundedCornerShape(4.dp), 216 | onClick = { 217 | backupsViewModel.restore(context, slotSuffix) 218 | navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") { 219 | popUpTo("slot{slotSuffix}") 220 | } 221 | }, 222 | enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none { currentBackup.hashes.get(it) != null && backupsViewModel.backupPartitions[it] == null } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty()) 223 | ) { 224 | Text(stringResource(R.string.restore)) 225 | } 226 | } else { 227 | FlashList( 228 | stringResource(R.string.restore), 229 | backupsViewModel.restoreOutput 230 | ) { 231 | AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) { 232 | Column { 233 | if (backupsViewModel.wasRestored != false) { 234 | OutlinedButton( 235 | modifier = Modifier 236 | .fillMaxWidth(), 237 | shape = RoundedCornerShape(4.dp), 238 | onClick = { navController.navigate("reboot") } 239 | ) { 240 | Text(stringResource(R.string.reboot)) 241 | } 242 | } 243 | } 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.error 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.height 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Warning 12 | import androidx.compose.material3.ExperimentalMaterial3Api 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.Scaffold 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.unit.dp 21 | import com.github.capntrips.kernelflasher.ui.theme.Orange500 22 | 23 | @ExperimentalMaterial3Api 24 | @Composable 25 | fun ErrorScreen(message: String) { 26 | Scaffold { paddingValues -> 27 | Box( 28 | contentAlignment = Alignment.Center, 29 | modifier = Modifier 30 | .padding(paddingValues) 31 | .fillMaxSize() 32 | ) { 33 | Column(horizontalAlignment = Alignment.CenterHorizontally) { 34 | Icon( 35 | Icons.Filled.Warning, 36 | modifier = Modifier 37 | .width(48.dp) 38 | .height(48.dp), 39 | tint = Orange500, 40 | contentDescription = message 41 | ) 42 | Spacer(Modifier.height(8.dp)) 43 | Text( 44 | message, 45 | modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp), 46 | style = MaterialTheme.typography.titleLarge, 47 | ) 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.main 2 | 3 | import android.os.Build 4 | import androidx.compose.animation.AnimatedVisibility 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.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.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.res.stringResource 19 | import androidx.compose.ui.unit.dp 20 | import androidx.navigation.NavController 21 | import com.github.capntrips.kernelflasher.R 22 | import com.github.capntrips.kernelflasher.ui.components.DataCard 23 | import com.github.capntrips.kernelflasher.ui.components.DataRow 24 | import com.github.capntrips.kernelflasher.ui.components.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 { mutableStateOf(0) } 35 | DataRow(stringResource(R.string.model), "${Build.MODEL} (${Build.DEVICE})", mutableMaxWidth = cardWidth) 36 | DataRow(stringResource(R.string.build_number), Build.ID, mutableMaxWidth = cardWidth) 37 | DataRow(stringResource(R.string.kernel_version), viewModel.kernelVersion, mutableMaxWidth = cardWidth, clickable = true) 38 | DataRow(stringResource(R.string.slot_suffix), viewModel.slotSuffix, mutableMaxWidth = cardWidth) 39 | } 40 | Spacer(Modifier.height(16.dp)) 41 | SlotCard( 42 | title = stringResource(R.string.slot_a), 43 | viewModel = viewModel.slotA, 44 | navController = navController 45 | ) 46 | Spacer(Modifier.height(16.dp)) 47 | SlotCard( 48 | title = stringResource(R.string.slot_b), 49 | viewModel = viewModel.slotB, 50 | navController = navController 51 | ) 52 | Spacer(Modifier.height(16.dp)) 53 | AnimatedVisibility(!viewModel.isRefreshing) { 54 | OutlinedButton( 55 | modifier = Modifier 56 | .fillMaxWidth(), 57 | shape = RoundedCornerShape(4.dp), 58 | onClick = { navController.navigate("backups") } 59 | ) { 60 | Text(stringResource(R.string.backups)) 61 | } 62 | } 63 | AnimatedVisibility(!viewModel.isRefreshing) { 64 | OutlinedButton( 65 | modifier = Modifier 66 | .fillMaxWidth(), 67 | shape = RoundedCornerShape(4.dp), 68 | onClick = { navController.navigate("updates") } 69 | ) { 70 | Text(stringResource(R.string.updates)) 71 | } 72 | } 73 | if (viewModel.hasRamoops) { 74 | OutlinedButton( 75 | modifier = Modifier 76 | .fillMaxWidth(), 77 | shape = RoundedCornerShape(4.dp), 78 | onClick = { viewModel.saveRamoops(context) } 79 | ) { 80 | Text(stringResource(R.string.save_ramoops)) 81 | } 82 | } 83 | OutlinedButton( 84 | modifier = Modifier 85 | .fillMaxWidth(), 86 | shape = RoundedCornerShape(4.dp), 87 | onClick = { viewModel.saveDmesg(context) } 88 | ) { 89 | Text(stringResource(R.string.save_dmesg)) 90 | } 91 | OutlinedButton( 92 | modifier = Modifier 93 | .fillMaxWidth(), 94 | shape = RoundedCornerShape(4.dp), 95 | onClick = { viewModel.saveLogcat(context) } 96 | ) { 97 | Text(stringResource(R.string.save_logcat)) 98 | } 99 | AnimatedVisibility(!viewModel.isRefreshing) { 100 | OutlinedButton( 101 | modifier = Modifier 102 | .fillMaxWidth(), 103 | shape = RoundedCornerShape(4.dp), 104 | onClick = { navController.navigate("reboot") } 105 | ) { 106 | Text(stringResource(R.string.reboot)) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.main 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.util.Log 6 | import android.widget.Toast 7 | import androidx.compose.runtime.MutableState 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import androidx.navigation.NavController 12 | import com.github.capntrips.kernelflasher.common.PartitionUtil 13 | import com.github.capntrips.kernelflasher.common.types.backups.Backup 14 | import com.github.capntrips.kernelflasher.ui.screens.backups.BackupsViewModel 15 | import com.github.capntrips.kernelflasher.ui.screens.reboot.RebootViewModel 16 | import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel 17 | import com.github.capntrips.kernelflasher.ui.screens.updates.UpdatesViewModel 18 | import com.topjohnwu.superuser.Shell 19 | import com.topjohnwu.superuser.nio.FileSystemManager 20 | import kotlinx.coroutines.Dispatchers 21 | import kotlinx.coroutines.launch 22 | import kotlinx.coroutines.withContext 23 | import java.io.File 24 | import java.time.LocalDateTime 25 | import java.time.format.DateTimeFormatter 26 | 27 | class MainViewModel( 28 | context: Context, 29 | fileSystemManager: FileSystemManager, 30 | private val navController: NavController 31 | ) : ViewModel() { 32 | companion object { 33 | const val TAG: String = "KernelFlasher/MainViewModel" 34 | } 35 | val slotSuffix: String 36 | 37 | val kernelVersion: String 38 | val slotA: SlotViewModel 39 | val slotB: SlotViewModel 40 | val backups: BackupsViewModel 41 | val updates: UpdatesViewModel 42 | val reboot: RebootViewModel 43 | val hasRamoops: Boolean 44 | 45 | private val _isRefreshing: MutableState = mutableStateOf(true) 46 | private var _error: String? = null 47 | private var _backups: MutableMap = mutableMapOf() 48 | 49 | val isRefreshing: Boolean 50 | get() = _isRefreshing.value 51 | val hasError: Boolean 52 | get() = _error != null 53 | val error: String 54 | get() = _error!! 55 | 56 | init { 57 | PartitionUtil.init(context, fileSystemManager) 58 | val partitionName = if (fileSystemManager.getFile("/dev/block/by-name/init_boot_a").exists()) "init_boot" else "boot" 59 | val bootA = PartitionUtil.findPartitionBlockDevice(context, partitionName, "_a")!! 60 | val bootB = PartitionUtil.findPartitionBlockDevice(context, partitionName, "_b")!! 61 | kernelVersion = Shell.cmd("echo $(uname -r) $(uname -v)").exec().out[0] 62 | slotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] 63 | backups = BackupsViewModel(context, fileSystemManager, navController, _isRefreshing, _backups) 64 | updates = UpdatesViewModel(context, fileSystemManager, navController, _isRefreshing) 65 | reboot = RebootViewModel(context, fileSystemManager, navController, _isRefreshing) 66 | slotA = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_a", "_a", bootA, _backups) 67 | if (slotA.hasError) { 68 | _error = slotA.error 69 | } 70 | slotB = SlotViewModel(context, fileSystemManager, navController, _isRefreshing, slotSuffix == "_b", "_b", bootB, _backups) 71 | if (slotB.hasError) { 72 | _error = slotB.error 73 | } 74 | 75 | hasRamoops = fileSystemManager.getFile("/sys/fs/pstore/console-ramoops-0").exists() 76 | _isRefreshing.value = false 77 | } 78 | 79 | fun refresh(context: Context) { 80 | launch { 81 | slotA.refresh(context) 82 | slotB.refresh(context) 83 | backups.refresh(context) 84 | } 85 | } 86 | 87 | private fun launch(block: suspend () -> Unit) { 88 | viewModelScope.launch(Dispatchers.IO) { 89 | viewModelScope.launch(Dispatchers.Main) { 90 | _isRefreshing.value = true 91 | } 92 | try { 93 | block() 94 | } catch (e: Exception) { 95 | withContext (Dispatchers.Main) { 96 | Log.e(TAG, e.message, e) 97 | navController.navigate("error/${e.message}") { 98 | popUpTo("main") 99 | } 100 | } 101 | } 102 | viewModelScope.launch(Dispatchers.Main) { 103 | _isRefreshing.value = false 104 | } 105 | } 106 | } 107 | 108 | @Suppress("SameParameterValue") 109 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 110 | Log.d(TAG, message) 111 | if (!shouldThrow) { 112 | viewModelScope.launch(Dispatchers.Main) { 113 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 114 | } 115 | } else { 116 | throw Exception(message) 117 | } 118 | } 119 | 120 | fun saveRamoops(context: Context) { 121 | launch { 122 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 123 | @SuppressLint("SdCardPath") 124 | val ramoops = File("/sdcard/Download/console-ramoops--$now.log") 125 | Shell.cmd("cp /sys/fs/pstore/console-ramoops-0 $ramoops").exec() 126 | if (ramoops.exists()) { 127 | log(context, "Saved ramoops to $ramoops") 128 | } else { 129 | log(context, "Failed to save $ramoops", shouldThrow = true) 130 | } 131 | } 132 | } 133 | 134 | fun saveDmesg(context: Context) { 135 | launch { 136 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 137 | @SuppressLint("SdCardPath") 138 | val dmesg = File("/sdcard/Download/dmesg--$now.log") 139 | Shell.cmd("dmesg > $dmesg").exec() 140 | if (dmesg.exists()) { 141 | log(context, "Saved dmesg to $dmesg") 142 | } else { 143 | log(context, "Failed to save $dmesg", shouldThrow = true) 144 | } 145 | } 146 | } 147 | 148 | fun saveLogcat(context: Context) { 149 | launch { 150 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 151 | @SuppressLint("SdCardPath") 152 | val logcat = File("/sdcard/Download/logcat--$now.log") 153 | Shell.cmd("logcat -d > $logcat").exec() 154 | if (logcat.exists()) { 155 | log(context, "Saved logcat to $logcat") 156 | } else { 157 | log(context, "Failed to save $logcat", shouldThrow = true) 158 | } 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.reboot 2 | 3 | import android.os.PowerManager 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.OutlinedButton 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.res.stringResource 13 | import androidx.compose.ui.unit.dp 14 | import androidx.navigation.NavController 15 | import com.github.capntrips.kernelflasher.R 16 | 17 | @Suppress("unused") 18 | @Composable 19 | fun ColumnScope.RebootContent( 20 | viewModel: RebootViewModel, 21 | @Suppress("UNUSED_PARAMETER") navController: NavController 22 | ) { 23 | val context = LocalContext.current 24 | OutlinedButton( 25 | modifier = Modifier 26 | .fillMaxWidth(), 27 | shape = RoundedCornerShape(4.dp), 28 | onClick = { viewModel.rebootSystem() } 29 | ) { 30 | Text(stringResource(R.string.reboot)) 31 | } 32 | if (context.getSystemService(PowerManager::class.java)?.isRebootingUserspaceSupported == true) { 33 | OutlinedButton( 34 | modifier = Modifier 35 | .fillMaxWidth(), 36 | shape = RoundedCornerShape(4.dp), 37 | onClick = { viewModel.rebootUserspace() } 38 | ) { 39 | Text(stringResource(R.string.reboot_userspace)) 40 | } 41 | } 42 | OutlinedButton( 43 | modifier = Modifier 44 | .fillMaxWidth(), 45 | shape = RoundedCornerShape(4.dp), 46 | onClick = { viewModel.rebootRecovery() } 47 | ) { 48 | Text(stringResource(R.string.reboot_recovery)) 49 | } 50 | OutlinedButton( 51 | modifier = Modifier 52 | .fillMaxWidth(), 53 | shape = RoundedCornerShape(4.dp), 54 | onClick = { viewModel.rebootBootloader() } 55 | ) { 56 | Text(stringResource(R.string.reboot_bootloader)) 57 | } 58 | OutlinedButton( 59 | modifier = Modifier 60 | .fillMaxWidth(), 61 | shape = RoundedCornerShape(4.dp), 62 | onClick = { viewModel.rebootDownload() } 63 | ) { 64 | Text(stringResource(R.string.reboot_download)) 65 | } 66 | OutlinedButton( 67 | modifier = Modifier 68 | .fillMaxWidth(), 69 | shape = RoundedCornerShape(4.dp), 70 | onClick = { viewModel.rebootEdl() } 71 | ) { 72 | Text(stringResource(R.string.reboot_edl)) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.reboot 2 | 3 | import android.content.Context 4 | import android.util.Log 5 | import androidx.compose.runtime.MutableState 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import androidx.navigation.NavController 9 | import com.topjohnwu.superuser.Shell 10 | import com.topjohnwu.superuser.nio.FileSystemManager 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | 15 | class RebootViewModel( 16 | @Suppress("unused", "UNUSED_PARAMETER") context: 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").submit() 53 | } 54 | } 55 | 56 | fun rebootSystem() { 57 | reboot() 58 | } 59 | 60 | fun rebootUserspace() { 61 | reboot("userspace") 62 | } 63 | 64 | fun rebootRecovery() { 65 | reboot("recovery") 66 | } 67 | 68 | fun rebootBootloader() { 69 | reboot("bootloader") 70 | } 71 | 72 | fun rebootDownload() { 73 | reboot("download") 74 | } 75 | 76 | fun rebootEdl() { 77 | reboot("edl") 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.slot 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.ExperimentalAnimationApi 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.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.capntrips.kernelflasher.R 22 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 23 | 24 | @ExperimentalUnitApi 25 | @ExperimentalAnimationApi 26 | @ExperimentalMaterial3Api 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 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.slot 2 | 3 | import androidx.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.material3.ButtonDefaults 14 | import androidx.compose.material3.Checkbox 15 | import androidx.compose.material3.ExperimentalMaterial3Api 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.OutlinedButton 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.alpha 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.platform.LocalContext 25 | import androidx.compose.ui.res.stringResource 26 | import androidx.compose.ui.unit.ExperimentalUnitApi 27 | import androidx.compose.ui.unit.dp 28 | import androidx.navigation.NavController 29 | import com.github.capntrips.kernelflasher.R 30 | import com.github.capntrips.kernelflasher.common.PartitionUtil 31 | import com.github.capntrips.kernelflasher.ui.components.DataCard 32 | import com.github.capntrips.kernelflasher.ui.components.FlashButton 33 | import com.github.capntrips.kernelflasher.ui.components.FlashList 34 | import com.github.capntrips.kernelflasher.ui.components.SlotCard 35 | 36 | @ExperimentalAnimationApi 37 | @ExperimentalUnitApi 38 | @ExperimentalMaterial3Api 39 | @Composable 40 | fun ColumnScope.SlotFlashContent( 41 | viewModel: SlotViewModel, 42 | slotSuffix: String, 43 | navController: NavController 44 | ) { 45 | val context = LocalContext.current 46 | if (!listOf("/flash/ak3", "/flash/image/flash", "/backup/backup").any { navController.currentDestination!!.route!!.endsWith(it) }) { 47 | SlotCard( 48 | title = stringResource(if (slotSuffix == "_a") R.string.slot_a else R.string.slot_b), 49 | viewModel = viewModel, 50 | navController = navController, 51 | isSlotScreen = true, 52 | showDlkm = false 53 | ) 54 | Spacer(Modifier.height(16.dp)) 55 | if (navController.currentDestination!!.route!! == "slot{slotSuffix}/flash") { 56 | DataCard (stringResource(R.string.flash)) 57 | Spacer(Modifier.height(5.dp)) 58 | FlashButton(stringResource(R.string.flash_ak3_zip), callback = { uri -> 59 | navController.navigate("slot$slotSuffix/flash/ak3") { 60 | popUpTo("slot{slotSuffix}") 61 | } 62 | viewModel.flashAk3(context, uri) 63 | }) 64 | OutlinedButton( 65 | modifier = Modifier 66 | .fillMaxWidth(), 67 | shape = RoundedCornerShape(4.dp), 68 | onClick = { 69 | navController.navigate("slot$slotSuffix/flash/image") 70 | } 71 | ) { 72 | Text(stringResource(R.string.flash_partition_image)) 73 | } 74 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/flash/image") { 75 | DataCard (stringResource(R.string.flash_partition_image)) 76 | Spacer(Modifier.height(5.dp)) 77 | for (partitionName in PartitionUtil.AvailablePartitions) { 78 | FlashButton(partitionName, callback = { uri -> 79 | navController.navigate("slot$slotSuffix/flash/image/flash") { 80 | popUpTo("slot{slotSuffix}") 81 | } 82 | viewModel.flashImage(context, uri, partitionName) 83 | }) 84 | } 85 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup") { 86 | DataCard (stringResource(R.string.backup)) 87 | Spacer(Modifier.height(5.dp)) 88 | val disabledColor = ButtonDefaults.buttonColors( 89 | Color.Transparent, 90 | MaterialTheme.colorScheme.onSurface 91 | ) 92 | for (partitionName in PartitionUtil.AvailablePartitions) { 93 | OutlinedButton( 94 | modifier = Modifier 95 | .fillMaxWidth() 96 | .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), 97 | shape = RoundedCornerShape(4.dp), 98 | colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor, 99 | onClick = { 100 | viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!! 101 | }, 102 | ) { 103 | Box(Modifier.fillMaxWidth()) { 104 | Checkbox(viewModel.backupPartitions[partitionName]!!, null, 105 | Modifier 106 | .align(Alignment.CenterStart) 107 | .offset(x = -(16.dp))) 108 | Text(partitionName, Modifier.align(Alignment.Center)) 109 | } 110 | } 111 | } 112 | OutlinedButton( 113 | modifier = Modifier 114 | .fillMaxWidth(), 115 | shape = RoundedCornerShape(4.dp), 116 | onClick = { 117 | viewModel.backup(context) 118 | navController.navigate("slot$slotSuffix/backup/backup") { 119 | popUpTo("slot{slotSuffix}") 120 | } 121 | }, 122 | enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty() 123 | ) { 124 | Text(stringResource(R.string.backup)) 125 | } 126 | } 127 | } else { 128 | Text("") 129 | FlashList( 130 | stringResource(if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup/backup") R.string.backup else R.string.flash), 131 | if (navController.currentDestination!!.route!!.contains("ak3")) viewModel.uiPrintedOutput else viewModel.flashOutput 132 | ) { 133 | AnimatedVisibility(!viewModel.isRefreshing && viewModel.wasFlashSuccess != null) { 134 | Column { 135 | if (navController.currentDestination!!.route!!.contains("ak3")) { 136 | OutlinedButton( 137 | modifier = Modifier 138 | .fillMaxWidth(), 139 | shape = RoundedCornerShape(4.dp), 140 | onClick = { viewModel.saveLog(context) } 141 | ) { 142 | if (navController.currentDestination!!.route!!.contains("ak3")) { 143 | Text(stringResource(R.string.save_ak3_log)) 144 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup/backup") { 145 | Text(stringResource(R.string.save_backup_log)) 146 | } else { 147 | Text(stringResource(R.string.save_flash_log)) 148 | } 149 | } 150 | } 151 | if (navController.currentDestination!!.route!!.contains("ak3")) { 152 | AnimatedVisibility(navController.currentDestination!!.route!! != "slot{slotSuffix}/backups/{backupId}/flash/ak3" && navController.previousBackStackEntry!!.destination.route!! != "slot{slotSuffix}/backups/{backupId}/flash/ak3" && viewModel.wasFlashSuccess != false) { 153 | OutlinedButton( 154 | modifier = Modifier 155 | .fillMaxWidth(), 156 | shape = RoundedCornerShape(4.dp), 157 | onClick = { 158 | viewModel.backupZip(context) { 159 | navController.navigate("slot$slotSuffix/backups") { 160 | popUpTo("slot{slotSuffix}") 161 | } 162 | } 163 | } 164 | ) { 165 | Text(stringResource(R.string.save_ak3_zip_as_backup)) 166 | } 167 | } 168 | } 169 | if (viewModel.wasFlashSuccess != false && navController.currentDestination!!.route!! == "slot{slotSuffix}/backup/backup") { 170 | OutlinedButton( 171 | modifier = Modifier 172 | .fillMaxWidth(), 173 | shape = RoundedCornerShape(4.dp), 174 | onClick = { navController.popBackStack() } 175 | ) { 176 | Text(stringResource(R.string.back)) 177 | } 178 | } else { 179 | OutlinedButton( 180 | modifier = Modifier 181 | .fillMaxWidth(), 182 | shape = RoundedCornerShape(4.dp), 183 | onClick = { navController.navigate("reboot") } 184 | ) { 185 | Text(stringResource(R.string.reboot)) 186 | } 187 | } 188 | } 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.slot 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.net.Uri 6 | import android.provider.OpenableColumns 7 | import android.util.Log 8 | import android.widget.Toast 9 | import androidx.compose.runtime.MutableState 10 | import androidx.compose.runtime.mutableStateListOf 11 | import androidx.compose.runtime.mutableStateMapOf 12 | import androidx.compose.runtime.mutableStateOf 13 | import androidx.compose.runtime.snapshots.SnapshotStateList 14 | import androidx.compose.runtime.snapshots.SnapshotStateMap 15 | import androidx.lifecycle.ViewModel 16 | import androidx.lifecycle.viewModelScope 17 | import androidx.navigation.NavController 18 | import com.github.capntrips.kernelflasher.common.PartitionUtil 19 | import com.github.capntrips.kernelflasher.common.extensions.ByteArray.toHex 20 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.inputStream 21 | import com.github.capntrips.kernelflasher.common.extensions.ExtendedFile.outputStream 22 | import com.github.capntrips.kernelflasher.common.types.backups.Backup 23 | import com.github.capntrips.kernelflasher.common.types.partitions.Partitions 24 | import com.topjohnwu.superuser.Shell 25 | import com.topjohnwu.superuser.nio.ExtendedFile 26 | import com.topjohnwu.superuser.nio.FileSystemManager 27 | import kotlinx.coroutines.Dispatchers 28 | import kotlinx.coroutines.launch 29 | import kotlinx.coroutines.withContext 30 | import kotlinx.serialization.encodeToString 31 | import kotlinx.serialization.json.Json 32 | import java.io.File 33 | import java.security.DigestOutputStream 34 | import java.security.MessageDigest 35 | import java.time.LocalDateTime 36 | import java.time.format.DateTimeFormatter 37 | import java.util.zip.ZipFile 38 | 39 | class SlotViewModel( 40 | context: Context, 41 | @Suppress("unused") private val fileSystemManager: FileSystemManager, 42 | private val navController: NavController, 43 | private val _isRefreshing: MutableState, 44 | val isActive: Boolean, 45 | val slotSuffix: String, 46 | private val boot: File, 47 | private val _backups: MutableMap 48 | ) : ViewModel() { 49 | companion object { 50 | const val TAG: String = "KernelFlasher/SlotState" 51 | } 52 | 53 | @Suppress("PropertyName") 54 | private var _sha1: String? = null 55 | var kernelVersion: String? = null 56 | var hasVendorDlkm: Boolean = false 57 | var isVendorDlkmMapped: Boolean = false 58 | var isVendorDlkmMounted: Boolean = false 59 | @Suppress("PropertyName") 60 | private val _flashOutput: SnapshotStateList = mutableStateListOf() 61 | private val _wasFlashSuccess: MutableState = mutableStateOf(null) 62 | private val _backupPartitions: SnapshotStateMap = mutableStateMapOf() 63 | private var wasSlotReset: Boolean = false 64 | private var flashUri: Uri? = null 65 | private var flashFilename: String? = null 66 | private val hashAlgorithm: String = "SHA-256" 67 | private var inInit = true 68 | private var _error: String? = null 69 | 70 | private val STOCK_MAGISKBOOT = "/data/adb/magisk/magiskboot" 71 | private var magiskboot: String = STOCK_MAGISKBOOT 72 | 73 | val sha1: String 74 | get() = _sha1!! 75 | val flashOutput: List 76 | get() = _flashOutput 77 | val uiPrintedOutput: List 78 | get() = _flashOutput.filter { it.startsWith("ui_print") }.map{ it.substring("ui_print".length + 1) } 79 | val wasFlashSuccess: Boolean? 80 | get() = _wasFlashSuccess.value 81 | val backupPartitions: MutableMap 82 | get() = _backupPartitions 83 | val isRefreshing: Boolean 84 | get() = _isRefreshing.value 85 | val hasError: Boolean 86 | get() = _error != null 87 | val error: String 88 | get() = _error!! 89 | 90 | init { 91 | refresh(context) 92 | } 93 | 94 | fun refresh(context: Context) { 95 | // init magiskboot 96 | if (!File(STOCK_MAGISKBOOT).exists()) { 97 | magiskboot = context.filesDir.absolutePath + File.separator + "magiskboot" 98 | } 99 | 100 | Shell.cmd("$magiskboot unpack $boot").exec() 101 | 102 | val ramdisk = File(context.filesDir, "ramdisk.cpio") 103 | 104 | var vendorDlkm = PartitionUtil.findPartitionBlockDevice(context, "vendor_dlkm", slotSuffix) 105 | hasVendorDlkm = vendorDlkm != null 106 | if (hasVendorDlkm) { 107 | isVendorDlkmMapped = vendorDlkm?.exists() == true 108 | if (isVendorDlkmMapped) { 109 | isVendorDlkmMounted = isPartitionMounted(vendorDlkm!!) 110 | if (!isVendorDlkmMounted) { 111 | vendorDlkm = fileSystemManager.getFile("/dev/block/mapper/vendor_dlkm-verity") 112 | isVendorDlkmMounted = isPartitionMounted(vendorDlkm) 113 | } 114 | } else { 115 | isVendorDlkmMounted = false 116 | } 117 | } 118 | 119 | val magiskboot = fileSystemManager.getFile(magiskboot) 120 | if (magiskboot.exists()) { 121 | if (ramdisk.exists()) { 122 | when (Shell.cmd("$magiskboot cpio ramdisk.cpio test").exec().code) { 123 | 0 -> _sha1 = Shell.cmd("$magiskboot sha1 $boot").exec().out.firstOrNull() 124 | 1 -> _sha1 = Shell.cmd("$magiskboot cpio ramdisk.cpio sha1").exec().out.firstOrNull() 125 | else -> log(context, "Invalid boot.img", shouldThrow = true) 126 | } 127 | } else { 128 | // boot.img v4 has no ramdisk! 129 | _sha1 = Shell.cmd("$magiskboot sha1 $boot").exec().out.firstOrNull() 130 | } 131 | Shell.cmd("$magiskboot cleanup").exec() 132 | } else { 133 | log(context, "magiskboot is missing", shouldThrow = true) 134 | } 135 | 136 | PartitionUtil.AvailablePartitions.forEach { partitionName -> 137 | _backupPartitions[partitionName] = true 138 | } 139 | 140 | kernelVersion = null 141 | inInit = false 142 | } 143 | 144 | // TODO: use base class for common functions 145 | private fun launch(block: suspend () -> Unit) { 146 | viewModelScope.launch(Dispatchers.IO) { 147 | _isRefreshing.value = true 148 | try { 149 | block() 150 | } catch (e: Exception) { 151 | withContext (Dispatchers.Main) { 152 | Log.e(TAG, e.message, e) 153 | navController.navigate("error/${e.message}") { 154 | popUpTo("main") 155 | } 156 | } 157 | } 158 | _isRefreshing.value = false 159 | } 160 | } 161 | 162 | // TODO: use base class for common functions 163 | @Suppress("SameParameterValue") 164 | private fun log(context: Context, message: String, shouldThrow: Boolean = false) { 165 | Log.d(TAG, message) 166 | if (!shouldThrow) { 167 | viewModelScope.launch(Dispatchers.Main) { 168 | Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 169 | } 170 | } else { 171 | if (inInit) { 172 | _error = message 173 | } else { 174 | throw Exception(message) 175 | } 176 | } 177 | } 178 | 179 | private fun uiPrint(message: String) { 180 | viewModelScope.launch(Dispatchers.Main) { 181 | _flashOutput.add("ui_print $message") 182 | _flashOutput.add(" ui_print") 183 | } 184 | } 185 | 186 | // TODO: use base class for common functions 187 | private fun addMessage(message: String) { 188 | viewModelScope.launch(Dispatchers.Main) { 189 | _flashOutput.add(message) 190 | } 191 | } 192 | 193 | private fun clearTmp(context: Context) { 194 | if (flashFilename != null) { 195 | val zip = File(context.filesDir, flashFilename!!) 196 | if (zip.exists()) { 197 | zip.delete() 198 | } 199 | } 200 | } 201 | 202 | @Suppress("FunctionName") 203 | private fun _clearFlash() { 204 | _flashOutput.clear() 205 | _wasFlashSuccess.value = null 206 | } 207 | 208 | fun clearFlash(context: Context) { 209 | _clearFlash() 210 | PartitionUtil.AvailablePartitions.forEach { partitionName -> 211 | _backupPartitions[partitionName] = true 212 | } 213 | launch { 214 | clearTmp(context) 215 | } 216 | } 217 | 218 | // TODO: use base class for common functions 219 | @SuppressLint("SdCardPath") 220 | fun saveLog(context: Context) { 221 | launch { 222 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 223 | val logName = if (navController.currentDestination!!.route!!.contains("ak3")) { 224 | "ak3" 225 | } else if (navController.currentDestination!!.route!! == "slot{slotSuffix}/backup") { 226 | "backup" 227 | } else { 228 | "flash" 229 | } 230 | val log = File("/sdcard/Download/$logName-log--$now.log") 231 | if (navController.currentDestination!!.route!!.contains("ak3")) { 232 | log.writeText(flashOutput.filter { !it.matches("""progress [\d.]* [\d.]*""".toRegex()) }.joinToString("\n").replace("""ui_print (.*)\n {6}ui_print""".toRegex(), "$1")) 233 | } else { 234 | log.writeText(flashOutput.joinToString("\n")) 235 | } 236 | if (log.exists()) { 237 | log(context, "Saved $logName log to $log") 238 | } else { 239 | log(context, "Failed to save $log", shouldThrow = true) 240 | } 241 | } 242 | } 243 | 244 | @Suppress("FunctionName", "SameParameterValue") 245 | private fun _getKernel(context: Context) { 246 | Shell.cmd("$magiskboot unpack $boot").exec() 247 | val kernel = File(context.filesDir, "kernel") 248 | if (kernel.exists()) { 249 | val result = Shell.cmd("strings kernel | grep -E -m1 'Linux version.*#' | cut -d\\ -f3-").exec().out 250 | if (result.isNotEmpty()) { 251 | kernelVersion = result[0].replace("""\(.+\)""".toRegex(), "").replace("""\s+""".toRegex(), " ") 252 | } 253 | } 254 | Shell.cmd("$magiskboot cleanup").exec() 255 | } 256 | 257 | fun getKernel(context: Context) { 258 | launch { 259 | _getKernel(context) 260 | } 261 | } 262 | 263 | private fun isPartitionMounted(partition: File): Boolean { 264 | @Suppress("LiftReturnOrAssignment") 265 | if (partition.exists()) { 266 | val dmPath = Shell.cmd("readlink -f $partition").exec().out[0] 267 | val mounts = Shell.cmd("mount | grep -w $dmPath").exec().out 268 | return mounts.isNotEmpty() 269 | } else { 270 | return false 271 | } 272 | } 273 | 274 | fun unmountVendorDlkm(context: Context) { 275 | launch { 276 | val httools = File(context.filesDir, "httools_static") 277 | Shell.cmd("$httools umount vendor_dlkm").exec() 278 | refresh(context) 279 | } 280 | } 281 | 282 | fun mountVendorDlkm(context: Context) { 283 | launch { 284 | val httools = File(context.filesDir, "httools_static") 285 | Shell.cmd("$httools mount vendor_dlkm").exec() 286 | refresh(context) 287 | } 288 | } 289 | 290 | fun unmapVendorDlkm(context: Context) { 291 | launch { 292 | val lptools = File(context.filesDir, "lptools_static") 293 | val mapperDir = "/dev/block/mapper" 294 | val vendorDlkm = fileSystemManager.getFile(mapperDir, "vendor_dlkm$slotSuffix") 295 | if (vendorDlkm.exists()) { 296 | val vendorDlkmVerity = fileSystemManager.getFile(mapperDir, "vendor_dlkm-verity") 297 | if (vendorDlkmVerity.exists()) { 298 | Shell.cmd("$lptools unmap vendor_dlkm-verity").exec() 299 | } else { 300 | Shell.cmd("$lptools unmap vendor_dlkm$slotSuffix").exec() 301 | } 302 | } 303 | refresh(context) 304 | } 305 | } 306 | 307 | fun mapVendorDlkm(context: Context) { 308 | launch { 309 | val lptools = File(context.filesDir, "lptools_static") 310 | Shell.cmd("$lptools map vendor_dlkm$slotSuffix").exec() 311 | refresh(context) 312 | } 313 | } 314 | 315 | private fun backupPartition(partition: ExtendedFile, destination: ExtendedFile): String? { 316 | if (partition.exists()) { 317 | val messageDigest = MessageDigest.getInstance(hashAlgorithm) 318 | partition.inputStream().use { inputStream -> 319 | destination.outputStream().use { outputStream -> 320 | DigestOutputStream(outputStream, messageDigest).use { digestOutputStream -> 321 | inputStream.copyTo(digestOutputStream) 322 | } 323 | } 324 | } 325 | return messageDigest.digest().toHex() 326 | } 327 | return null 328 | } 329 | 330 | private fun backupPartitions(context: Context, destination: ExtendedFile): Partitions? { 331 | val partitions = HashMap() 332 | for (partitionName in PartitionUtil.PartitionNames) { 333 | if (_backupPartitions[partitionName] == true) { 334 | val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) 335 | if (blockDevice != null) { 336 | addMessage("Saving $partitionName") 337 | val hash = backupPartition(blockDevice, destination.getChildFile("$partitionName.img")) 338 | if (hash != null) { 339 | partitions[partitionName] = hash 340 | } 341 | } 342 | } 343 | } 344 | if (partitions.isNotEmpty()) { 345 | return Partitions.from(partitions) 346 | } 347 | return null 348 | } 349 | 350 | private fun createBackupDir(context: Context, now: String): ExtendedFile { 351 | @SuppressLint("SdCardPath") 352 | val externalDir = fileSystemManager.getFile("/sdcard/KernelFlasher") 353 | if (!externalDir.exists()) { 354 | if (!externalDir.mkdir()) { 355 | log(context, "Failed to create KernelFlasher dir on /sdcard", shouldThrow = true) 356 | } 357 | } 358 | val backupsDir = externalDir.getChildFile("backups") 359 | if (!backupsDir.exists()) { 360 | if (!backupsDir.mkdir()) { 361 | log(context, "Failed to create backups dir", shouldThrow = true) 362 | } 363 | } 364 | val backupDir = backupsDir.getChildFile(now) 365 | if (backupDir.exists()) { 366 | log(context, "Backup $now already exists", shouldThrow = true) 367 | } else { 368 | if (!backupDir.mkdir()) { 369 | log(context, "Failed to create backup dir", shouldThrow = true) 370 | } 371 | } 372 | return backupDir 373 | } 374 | 375 | fun backup(context: Context) { 376 | launch { 377 | _clearFlash() 378 | val currentKernelVersion = if (kernelVersion != null) { 379 | kernelVersion 380 | } else if (isActive) { 381 | System.getProperty("os.version")!! 382 | } else { 383 | _getKernel(context) 384 | kernelVersion 385 | } 386 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 387 | val backupDir = createBackupDir(context, now) 388 | addMessage("Saving backup $now") 389 | val hashes = backupPartitions(context, backupDir) 390 | if (hashes == null) { 391 | log(context, "No partitions saved", shouldThrow = true) 392 | } 393 | val jsonFile = backupDir.getChildFile("backup.json") 394 | val backup = Backup(now, "raw", currentKernelVersion!!, sha1, null, hashes, hashAlgorithm) 395 | val indentedJson = Json { prettyPrint = true } 396 | @Suppress("BlockingMethodInNonBlockingContext") 397 | jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } 398 | _backups[now] = backup 399 | addMessage("Backup $now saved") 400 | _wasFlashSuccess.value = true 401 | } 402 | } 403 | 404 | fun backupZip(context: Context, callback: () -> Unit) { 405 | launch { 406 | @Suppress("BlockingMethodInNonBlockingContext") 407 | val source = context.contentResolver.openInputStream(flashUri!!) 408 | if (source != null) { 409 | _getKernel(context) 410 | val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) 411 | val backupDir = createBackupDir(context, now) 412 | val jsonFile = backupDir.getChildFile("backup.json") 413 | val backup = Backup(now, "ak3", kernelVersion!!, null, flashFilename) 414 | val indentedJson = Json { prettyPrint = true } 415 | @Suppress("BlockingMethodInNonBlockingContext") 416 | jsonFile.outputStream().use { it.write(indentedJson.encodeToString(backup).toByteArray(Charsets.UTF_8)) } 417 | val destination = backupDir.getChildFile(flashFilename!!) 418 | source.use { inputStream -> 419 | @Suppress("BlockingMethodInNonBlockingContext") 420 | destination.outputStream().use { outputStream -> 421 | inputStream.copyTo(outputStream) 422 | } 423 | } 424 | _backups[now] = backup 425 | withContext (Dispatchers.Main) { 426 | callback.invoke() 427 | } 428 | } else { 429 | log(context, "AK3 zip is missing", shouldThrow = true) 430 | } 431 | } 432 | } 433 | 434 | private fun resetSlot() { 435 | val activeSlotSuffix = Shell.cmd("getprop ro.boot.slot_suffix").exec().out[0] 436 | val newSlot = if (activeSlotSuffix == "_a") "_b" else "_a" 437 | val isMagisk = Shell.cmd("which magisk").exec().isSuccess; 438 | val resetprop = if (isMagisk) "magisk resetprop" else "resetprop" 439 | Shell.cmd("$resetprop -n ro.boot.slot_suffix $newSlot").exec() 440 | wasSlotReset = !wasSlotReset 441 | } 442 | 443 | @Suppress("FunctionName") 444 | private suspend fun _checkZip(context: Context, zip: File, callback: (() -> Unit)? = null) { 445 | if (zip.exists()) { 446 | try { 447 | @Suppress("BlockingMethodInNonBlockingContext") 448 | val zipFile = ZipFile(zip) 449 | zipFile.use { z -> 450 | if (z.getEntry("anykernel.sh") == null) { 451 | log(context, "Invalid AK3 zip", shouldThrow = true) 452 | } 453 | withContext (Dispatchers.Main) { 454 | callback?.invoke() 455 | } 456 | } 457 | } catch (e: Exception) { 458 | zip.delete() 459 | throw e 460 | } 461 | } else { 462 | log(context, "Failed to save zip", shouldThrow = true) 463 | } 464 | } 465 | 466 | @Suppress("FunctionName") 467 | private fun _copyFile(context: Context, currentBackup: String, filename: String) { 468 | flashUri = null 469 | flashFilename = filename 470 | @SuppressLint("SdCardPath") 471 | val externalDir = File("/sdcard/KernelFlasher") 472 | val backupsDir = fileSystemManager.getFile("$externalDir/backups") 473 | val backupDir = backupsDir.getChildFile(currentBackup) 474 | if (!backupDir.exists()) { 475 | log(context, "Backup $currentBackup does not exists", shouldThrow = true) 476 | } 477 | val source = backupDir.getChildFile(flashFilename!!) 478 | val zip = File(context.filesDir, flashFilename!!) 479 | @Suppress("BlockingMethodInNonBlockingContext") 480 | source.newInputStream().use { inputStream -> 481 | @Suppress("BlockingMethodInNonBlockingContext") 482 | zip.outputStream().use { outputStream -> 483 | inputStream.copyTo(outputStream) 484 | } 485 | } 486 | } 487 | 488 | @Suppress("FunctionName") 489 | private fun _copyFile(context: Context, uri: Uri) { 490 | flashUri = uri 491 | flashFilename = context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> 492 | if (!cursor.moveToFirst()) return@use null 493 | val name = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) 494 | return@use cursor.getString(name) 495 | } ?: "ak3.zip" 496 | @Suppress("BlockingMethodInNonBlockingContext") 497 | val source = context.contentResolver.openInputStream(uri) 498 | val file = File(context.filesDir, flashFilename!!) 499 | source.use { inputStream -> 500 | file.outputStream().use { outputStream -> 501 | inputStream?.copyTo(outputStream) 502 | } 503 | } 504 | } 505 | 506 | @Suppress("FunctionName") 507 | private suspend fun _flashAk3(context: Context) { 508 | if (!isActive) { 509 | resetSlot() 510 | } 511 | val zip = File(context.filesDir.canonicalPath, flashFilename!!) 512 | _checkZip(context, zip) 513 | try { 514 | if (zip.exists()) { 515 | _wasFlashSuccess.value = false 516 | val files = File(context.filesDir.canonicalPath) 517 | val flashScript = File(files, "flash_ak3.sh") 518 | val result = Shell.Builder.create().setFlags(Shell.FLAG_MOUNT_MASTER or Shell.FLAG_REDIRECT_STDERR).build().newJob().add("F=$files Z=\"$zip\" /system/bin/sh $flashScript").to(flashOutput).exec() 519 | if (result.isSuccess) { 520 | log(context, "Kernel flashed successfully") 521 | _wasFlashSuccess.value = true 522 | } else { 523 | log(context, "Failed to flash zip", shouldThrow = false) 524 | } 525 | clearTmp(context) 526 | } else { 527 | log(context, "AK3 zip is missing", shouldThrow = true) 528 | } 529 | } catch (e: Exception) { 530 | clearFlash(context) 531 | throw e 532 | } finally { 533 | uiPrint("") 534 | if (wasSlotReset) { 535 | resetSlot() 536 | } 537 | } 538 | } 539 | 540 | fun flashAk3(context: Context, currentBackup: String, filename: String) { 541 | launch { 542 | _clearFlash() 543 | _copyFile(context, currentBackup, filename) 544 | _flashAk3(context) 545 | } 546 | } 547 | 548 | fun flashAk3(context: Context, uri: Uri) { 549 | launch { 550 | _clearFlash() 551 | _copyFile(context, uri) 552 | _flashAk3(context) 553 | } 554 | } 555 | 556 | fun flashImage(context: Context, uri: Uri, partitionName: String) { 557 | launch { 558 | _clearFlash() 559 | addMessage("Copying image ...") 560 | _copyFile(context, uri) 561 | if (!isActive) { 562 | resetSlot() 563 | } 564 | val image = fileSystemManager.getFile(context.filesDir, flashFilename!!) 565 | try { 566 | if (image.exists()) { 567 | addMessage("Copied $flashFilename") 568 | _wasFlashSuccess.value = false 569 | addMessage("Flashing $flashFilename to $partitionName ...") 570 | val blockDevice = PartitionUtil.findPartitionBlockDevice(context, partitionName, slotSuffix) 571 | if (blockDevice != null && blockDevice.exists()) { 572 | if (PartitionUtil.isPartitionLogical(context, partitionName)) { 573 | PartitionUtil.flashLogicalPartition(context, image, blockDevice, partitionName, slotSuffix, hashAlgorithm) { message -> 574 | addMessage(message) 575 | } 576 | } else { 577 | PartitionUtil.flashBlockDevice(image, blockDevice, hashAlgorithm) 578 | } 579 | } else { 580 | log(context, "Partition $partitionName was not found", shouldThrow = true) 581 | } 582 | addMessage("Flashed $flashFilename to $partitionName") 583 | addMessage("Cleaning up ...") 584 | clearTmp(context) 585 | addMessage("Done.") 586 | _wasFlashSuccess.value = true 587 | } else { 588 | log(context, "Partition image is missing", shouldThrow = true) 589 | } 590 | } catch (e: Exception) { 591 | clearFlash(context) 592 | throw e 593 | } finally { 594 | addMessage("") 595 | if (wasSlotReset) { 596 | resetSlot() 597 | } 598 | } 599 | } 600 | } 601 | } 602 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.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.capntrips.kernelflasher.R 23 | import kotlinx.serialization.ExperimentalSerializationApi 24 | 25 | @Suppress("unused") 26 | @ExperimentalSerializationApi 27 | @ExperimentalMaterial3Api 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/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.foundation.layout.Spacer 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.LocalTextStyle 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.font.FontFamily 12 | import androidx.compose.ui.unit.ExperimentalUnitApi 13 | import androidx.compose.ui.unit.TextUnit 14 | import androidx.compose.ui.unit.TextUnitType 15 | import androidx.compose.ui.unit.dp 16 | import androidx.navigation.NavController 17 | import com.github.capntrips.kernelflasher.ui.components.DataCard 18 | 19 | @Suppress("unused") 20 | @ExperimentalUnitApi 21 | @ExperimentalMaterial3Api 22 | @Composable 23 | fun ColumnScope.UpdatesChangelogContent( 24 | viewModel: UpdatesViewModel, 25 | @Suppress("UNUSED_PARAMETER") navController: NavController 26 | ) { 27 | viewModel.currentUpdate?.let { currentUpdate -> 28 | DataCard(currentUpdate.kernelName) 29 | Spacer(Modifier.height(16.dp)) 30 | Text(viewModel.changelog!!, 31 | style = LocalTextStyle.current.copy( 32 | fontFamily = FontFamily.Monospace, 33 | fontSize = TextUnit(12.0f, TextUnitType.Sp), 34 | lineHeight = TextUnit(18.0f, TextUnitType.Sp) 35 | ) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.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.mutableStateOf 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.capntrips.kernelflasher.R 24 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer 25 | import com.github.capntrips.kernelflasher.ui.components.DataCard 26 | import com.github.capntrips.kernelflasher.ui.components.DataRow 27 | import com.github.capntrips.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 { mutableStateOf(0) } 53 | DataRow(stringResource(R.string.version), update.kernelVersion, mutableMaxWidth = cardWidth) 54 | DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(update.kernelDate), mutableMaxWidth = cardWidth) 55 | DataRow( 56 | label = stringResource(R.string.last_updated), 57 | value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!), 58 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 59 | labelStyle = MaterialTheme.typography.labelMedium.copy( 60 | fontStyle = FontStyle.Italic 61 | ), 62 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 63 | valueStyle = MaterialTheme.typography.titleSmall.copy( 64 | fontStyle = FontStyle.Italic 65 | ), 66 | mutableMaxWidth = cardWidth 67 | ) 68 | } 69 | } 70 | } 71 | AnimatedVisibility(!viewModel.isRefreshing) { 72 | Column { 73 | Spacer(Modifier.height(12.dp)) 74 | OutlinedButton( 75 | modifier = Modifier 76 | .fillMaxWidth(), 77 | shape = RoundedCornerShape(4.dp), 78 | onClick = { navController.navigate("updates/add") } 79 | ) { 80 | Text(stringResource(R.string.add)) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesUrlState.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | @Suppress("unused") 4 | class UpdatesUrlState { 5 | // TODO: validate the url field 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.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.mutableStateOf 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.capntrips.kernelflasher.R 24 | import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer 25 | import com.github.capntrips.kernelflasher.ui.components.DataCard 26 | import com.github.capntrips.kernelflasher.ui.components.DataRow 27 | import kotlinx.serialization.ExperimentalSerializationApi 28 | 29 | @ExperimentalSerializationApi 30 | @ExperimentalMaterial3Api 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 { mutableStateOf(0) } 40 | DataRow(stringResource(R.string.version), currentUpdate.kernelVersion, mutableMaxWidth = cardWidth) 41 | DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(currentUpdate.kernelDate), mutableMaxWidth = cardWidth) 42 | DataRow( 43 | label = stringResource(R.string.last_updated), 44 | value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!), 45 | labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 46 | labelStyle = MaterialTheme.typography.labelMedium.copy( 47 | fontStyle = FontStyle.Italic 48 | ), 49 | valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), 50 | valueStyle = MaterialTheme.typography.titleSmall.copy( 51 | fontStyle = FontStyle.Italic, 52 | ), 53 | mutableMaxWidth = cardWidth 54 | ) 55 | } 56 | AnimatedVisibility(!viewModel.isRefreshing) { 57 | Column { 58 | Spacer(Modifier.height(5.dp)) 59 | OutlinedButton( 60 | modifier = Modifier 61 | .fillMaxWidth(), 62 | shape = RoundedCornerShape(4.dp), 63 | onClick = { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } } 64 | ) { 65 | Text(stringResource(R.string.changelog)) 66 | } 67 | // TODO: add download progress indicator 68 | OutlinedButton( 69 | modifier = Modifier 70 | .fillMaxWidth(), 71 | shape = RoundedCornerShape(4.dp), 72 | onClick = { viewModel.downloadKernel(context) } 73 | ) { 74 | Text(stringResource(R.string.download)) 75 | } 76 | OutlinedButton( 77 | modifier = Modifier 78 | .fillMaxWidth(), 79 | shape = RoundedCornerShape(4.dp), 80 | onClick = { viewModel.update() } 81 | ) { 82 | Text(stringResource(R.string.check_for_updates)) 83 | } 84 | OutlinedButton( 85 | modifier = Modifier 86 | .fillMaxWidth(), 87 | shape = RoundedCornerShape(4.dp), 88 | onClick = { viewModel.delete { navController.popBackStack() } } 89 | ) { 90 | Text(stringResource(R.string.delete)) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.screens.updates 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ContentValues 5 | import android.content.Context 6 | import android.net.Uri 7 | import android.os.Environment 8 | import android.provider.MediaStore 9 | import android.util.Log 10 | import android.widget.Toast 11 | import androidx.compose.runtime.MutableState 12 | import androidx.compose.runtime.mutableStateListOf 13 | import androidx.compose.runtime.snapshots.SnapshotStateList 14 | import androidx.lifecycle.ViewModel 15 | import androidx.lifecycle.viewModelScope 16 | import androidx.navigation.NavController 17 | import androidx.room.Room 18 | import com.github.capntrips.kernelflasher.common.types.room.AppDatabase 19 | import com.github.capntrips.kernelflasher.common.types.room.updates.Update 20 | import com.github.capntrips.kernelflasher.common.types.room.updates.UpdateSerializer 21 | import com.topjohnwu.superuser.nio.FileSystemManager 22 | import kotlinx.coroutines.Dispatchers 23 | import kotlinx.coroutines.launch 24 | import kotlinx.coroutines.withContext 25 | import kotlinx.serialization.ExperimentalSerializationApi 26 | import kotlinx.serialization.json.Json 27 | import okhttp3.OkHttpClient 28 | import okhttp3.Request 29 | import java.io.IOException 30 | import java.text.SimpleDateFormat 31 | import java.util.Date 32 | import java.util.Locale 33 | import kotlin.io.path.Path 34 | import kotlin.io.path.name 35 | 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 | @Suppress("unused") 44 | const val TAG: String = "KernelFlasher/UpdatesState" 45 | val lastUpdatedFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) 46 | } 47 | 48 | private val client = OkHttpClient() 49 | private val db = 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 | @ExperimentalSerializationApi 109 | @Suppress("BlockingMethodInNonBlockingContext") 110 | fun add(url: String, callback: (updateId: Int) -> Unit) { 111 | launch { 112 | val request = Request.Builder() 113 | .url(url) 114 | .build() 115 | 116 | client.newCall(request).execute().use { response -> 117 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 118 | val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string()) 119 | update.updateUri = url 120 | update.lastUpdated = Date() 121 | val updateId = updateDao.insert(update).toInt() 122 | val inserted = updateDao.load(updateId) 123 | withContext (Dispatchers.Main) { 124 | _updates.add(inserted) 125 | callback.invoke(updateId) 126 | } 127 | } 128 | } 129 | } 130 | 131 | @ExperimentalSerializationApi 132 | @Suppress("BlockingMethodInNonBlockingContext") 133 | fun update() { 134 | launch { 135 | val request = Request.Builder() 136 | .url(currentUpdate!!.updateUri!!) 137 | .build() 138 | 139 | client.newCall(request).execute().use { response -> 140 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 141 | val update: Update = Json.decodeFromString(UpdateSerializer, response.body!!.string()) 142 | currentUpdate!!.let { 143 | withContext (Dispatchers.Main) { 144 | it.kernelName = update.kernelName 145 | it.kernelVersion = update.kernelVersion 146 | it.kernelLink = update.kernelLink 147 | it.kernelChangelogUrl = update.kernelChangelogUrl 148 | it.kernelDate = update.kernelDate 149 | it.kernelSha1 = update.kernelSha1 150 | it.supportLink = update.supportLink 151 | it.lastUpdated = Date() 152 | viewModelScope.launch(Dispatchers.IO) { 153 | updateDao.update(it) 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | 161 | @ExperimentalSerializationApi 162 | @Suppress("BlockingMethodInNonBlockingContext") 163 | fun downloadChangelog(callback: () -> Unit) { 164 | launch { 165 | val request = Request.Builder() 166 | .url(currentUpdate!!.kernelChangelogUrl) 167 | .build() 168 | 169 | client.newCall(request).execute().use { response -> 170 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 171 | changelog = response.body!!.string() 172 | withContext (Dispatchers.Main) { 173 | callback.invoke() 174 | } 175 | } 176 | } 177 | } 178 | 179 | private fun insertDownload(context: Context, filename: String): Uri? { 180 | val resolver = context.contentResolver 181 | val values = ContentValues() 182 | values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename) 183 | values.put(MediaStore.MediaColumns.MIME_TYPE, "application/zip") 184 | values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) 185 | return resolver.insert(MediaStore.Files.getContentUri("external"), values) 186 | } 187 | 188 | @SuppressLint("SdCardPath") 189 | @ExperimentalSerializationApi 190 | @Suppress("BlockingMethodInNonBlockingContext") 191 | fun downloadKernel(context: Context) { 192 | launch { 193 | val remoteUri = Uri.parse(currentUpdate!!.kernelLink) 194 | val filename = Path(remoteUri.path!!).name 195 | val localUri = insertDownload(context, filename) 196 | localUri!!.let { uri -> 197 | val request = Request.Builder() 198 | .url(remoteUri.toString()) 199 | .build() 200 | 201 | client.newCall(request).execute().use { response -> 202 | if (!response.isSuccessful) throw IOException("Unexpected response: $response") 203 | response.body!!.byteStream().use { inputStream -> 204 | context.contentResolver.openOutputStream(uri)!!.use { outputStream -> 205 | inputStream.copyTo(outputStream) 206 | } 207 | } 208 | log(context, "Saved $filename to Downloads") 209 | } 210 | } 211 | } 212 | } 213 | 214 | fun delete(callback: () -> Unit) { 215 | launch { 216 | updateDao.delete(currentUpdate!!) 217 | withContext (Dispatchers.Main) { 218 | _updates.remove(currentUpdate!!) 219 | callback.invoke() 220 | currentUpdate = null 221 | } 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Orange500 = Color(0xFFFF9800) 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.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 | darkTheme -> darkColorScheme() 25 | else -> lightColorScheme() 26 | } 27 | MaterialTheme( 28 | colorScheme = colorScheme, 29 | typography = Typography, 30 | content = content 31 | ) 32 | } -------------------------------------------------------------------------------- /app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.github.capntrips.kernelflasher.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | 5 | val Typography = Typography().copy() -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_splash_animation.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 17 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_splash_foreground.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 13 | 17 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | -------------------------------------------------------------------------------- /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 | 62 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | accompanist_version = '0.25.0' 4 | activity_version = '1.6.0' 5 | appcompat_version = '1.7.0-alpha01' 6 | compose_version = '1.3.0-rc01' 7 | compiler_version = '1.3.0' 8 | core_version = '1.9.0-rc01' 9 | libsu_version = '5.0.2' 10 | lifecycle_version = '2.5.1' 11 | material_version = '1.8.0-alpha01' 12 | material3_version = '1.0.0-rc01' 13 | nav_version = '2.5.2' 14 | okhttp_version = '4.10.0' 15 | room_version = '2.4.3' 16 | serialization_version = '1.4.0-RC' 17 | splashscreen_version = '1.0.0' 18 | } 19 | } 20 | 21 | plugins { 22 | id 'com.android.application' version '7.3.1' apply false 23 | id 'com.android.library' version '7.3.1' apply false 24 | id 'org.jetbrains.kotlin.android' version '1.7.10' apply false 25 | id 'org.jetbrains.kotlin.kapt' version '1.7.10' apply false 26 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.10' apply false 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tiann/KernelFlasher/d1623fe0ba8da17609aa859171ce1a36655625fe/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 06 09:30:10 CDT 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------