Compare commits

...

2 Commits

Author SHA1 Message Date
dependabot[bot]
4a22cd8489
Merge 18bbdb9cdf83c3987350d7c3746e0c9d1cd7ef67 into e681c8fbec51facd25a7254143c4032aac9aba3a 2025-02-19 04:49:46 +00:00
ukriu
e681c8fbec
manager: allow multiple modules to be installed sequentially (#2451)
cherry picked from KernelSU-Next by rifsxd at [commit
2f42ebd](2f42ebd620)
  
Looks like this:


https://github.com/user-attachments/assets/2c759305-be2f-4855-976a-a87b9d920177

Co-authored-by: Rifat Azad <rifat.44.azad.rifs@gmail.com>
2025-02-19 12:22:38 +08:00
3 changed files with 102 additions and 10 deletions

View File

@ -48,6 +48,7 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
@ -65,16 +66,54 @@ import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* @author weishu
* @date 2023/1/1.
*/
enum class FlashingStatus {
FLASHING,
SUCCESS,
FAILED
}
/**
* @author weishu
* @date 2023/1/1.
*/
// Lets you flash modules sequentially when mutiple zipUris are selected
fun flashModulesSequentially(
uris: List<Uri>,
onFinish: (Boolean, Int) -> Unit,
onStdout: (String) -> Unit,
onStderr: (String) -> Unit
) {
val iterator = uris.iterator()
// Start processing from the first module inside a coroutine
CoroutineScope(Dispatchers.IO).launch {
// Define the recursive function within the coroutine
suspend fun processNext() {
if (iterator.hasNext()) {
// Flash the current module
flashModule(iterator.next(), onFinish = { showReboot, code ->
// If successful, continue to the next one
if (code == 0) {
// Recursively call to process the next module
launch {
processNext()
}
} else {
onFinish(showReboot, code) // If failed, finish the process
}
}, onStdout, onStderr)
} else {
// No more modules to process, finish the process
onFinish(true, 0)
}
}
// Start the process
processNext()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@Destination<RootGraph>
@ -193,6 +232,8 @@ sealed class FlashIt : Parcelable {
data class FlashModule(val uri: Uri) : FlashIt()
data class FlashModules(val uris: List<Uri>) : FlashIt()
data object FlashRestore : FlashIt()
data object FlashUninstall : FlashIt()
@ -215,6 +256,10 @@ fun flashIt(
is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr)
is FlashIt.FlashModules -> {
flashModulesSequentially(flashIt.uris, onFinish, onStdout, onStderr)
}
FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr)
FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr)

View File

@ -36,6 +36,7 @@ import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Checkbox
@ -56,6 +57,7 @@ import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.rememberTopAppBarState
@ -103,6 +105,7 @@ import me.weishu.kernelsu.ui.util.hasMagisk
import me.weishu.kernelsu.ui.util.reboot
import me.weishu.kernelsu.ui.util.toggleModule
import me.weishu.kernelsu.ui.util.uninstallModule
import me.weishu.kernelsu.ui.util.getFileName
import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel
import me.weishu.kernelsu.ui.webui.WebUIActivity
import okhttp3.OkHttpClient
@ -132,6 +135,11 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
var zipUri by remember { mutableStateOf<Uri?>(null) }
var zipUris by remember { mutableStateOf<List<Uri>>(emptyList()) }
var showConfirmDialog by remember { mutableStateOf(false) }
val webUILauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { viewModel.fetchModuleList() }
@ -207,20 +215,27 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
return@rememberLauncherForActivityResult
}
val data = it.data ?: return@rememberLauncherForActivityResult
val uri = data.data ?: return@rememberLauncherForActivityResult
val clipData = data.clipData
navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri)))
val uris = mutableListOf<Uri>()
if (clipData != null) {
for (i in 0 until clipData.itemCount) {
clipData.getItemAt(i)?.uri?.let { uris.add(it) }
}
} else {
data.data?.let { uris.add(it) }
}
viewModel.markNeedRefresh()
Log.i("ModuleScreen", "select zip result: ${it.data}")
zipUris = uris
showConfirmDialog = uris.isNotEmpty()
}
ExtendedFloatingActionButton(
onClick = {
// select the zip file to install
// Select the zip files to install
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "application/zip"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}
selectZipLauncher.launch(intent)
},
@ -232,6 +247,36 @@ fun ModuleScreen(navigator: DestinationsNavigator) {
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
snackbarHost = { SnackbarHost(hostState = snackBarHost) }
) { innerPadding ->
// confirmation dialog
if (showConfirmDialog && zipUris.isNotEmpty()) {
val moduleNames = zipUris.joinToString("\n") { it.getFileName(context).toString() }
AlertDialog(
onDismissRequest = { showConfirmDialog = false },
confirmButton = {
TextButton(onClick = {
showConfirmDialog = false
navigator.navigate(FlashScreenDestination(FlashIt.FlashModules(zipUris)))
viewModel.markNeedRefresh()
}) {
Text(stringResource(R.string.confirm))
}
},
dismissButton = {
TextButton(onClick = { showConfirmDialog = false }) {
Text(stringResource(android.R.string.cancel))
}
},
title = { Text(stringResource(R.string.module)) },
text = {
Text(
stringResource(R.string.module_install_prompt_with_name, moduleNames)
)
}
)
}
when {
hasMagisk -> {
Box(

View File

@ -23,8 +23,10 @@
<string name="module_failed_to_disable">Failed to disable module: %s</string>
<string name="module_empty">No module installed</string>
<string name="module">Module</string>
<string name="module_install_prompt_with_name">Do you want to continue installing module %1$s?</string>
<string name="module_sort_action_first">Sort (Action first)</string>
<string name="module_sort_enabled_first">Sort (Enabled first)</string>
<string name="confirm">Confirm</string>
<string name="uninstall">Uninstall</string>
<string name="module_install">Install</string>
<string name="install">Install</string>