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