From ee1dc9e04fea2ee6d9821dd97072f576db2c7009 Mon Sep 17 00:00:00 2001 From: Bitmap Date: Sat, 9 Mar 2024 17:50:50 +0530 Subject: [PATCH] - Added ADB Setup - Added Command Executor for executing commands. - Added Generic Loading Dialog - Saved ADB path to storage - TODO : Need to add Device options if ADB setup is done - Updated kotlin_lint.yml --- .github/workflows/kotlin_lint.yml | 3 + src/jvmMain/kotlin/Main.kt | 87 +++++++++++++++---- .../kotlin/{ => command}/CommandBuilder.kt | 23 ++++- src/jvmMain/kotlin/command/CommandExecutor.kt | 55 ++++++++++++ .../kotlin/ui/components/ButtonWithToolTip.kt | 73 ++++++++++++++++ .../kotlin/ui/components/CheckboxWithText.kt | 1 + .../kotlin/ui/components/LoadingDialog.kt | 57 ++++++++++++ .../kotlin/ui/components/TextWithIcon.kt | 36 ++++++++ src/jvmMain/kotlin/utils/Constant.kt | 10 ++- src/jvmMain/kotlin/utils/Strings.kt | 8 +- src/jvmMain/resources/done.svg | 1 + src/jvmMain/resources/info.svg | 4 +- 12 files changed, 336 insertions(+), 22 deletions(-) rename src/jvmMain/kotlin/{ => command}/CommandBuilder.kt (79%) create mode 100644 src/jvmMain/kotlin/command/CommandExecutor.kt create mode 100644 src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt create mode 100644 src/jvmMain/kotlin/ui/components/LoadingDialog.kt create mode 100644 src/jvmMain/kotlin/ui/components/TextWithIcon.kt create mode 100644 src/jvmMain/resources/done.svg diff --git a/.github/workflows/kotlin_lint.yml b/.github/workflows/kotlin_lint.yml index 39d508e..ba83209 100644 --- a/.github/workflows/kotlin_lint.yml +++ b/.github/workflows/kotlin_lint.yml @@ -1,6 +1,9 @@ name: kotlin_lint on: + push: + branches: + - "*" pull_request: paths: - "**/*.kt" diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index f6eeaa1..f404a0a 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -17,13 +17,13 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.* +import command.CommandBuilder +import command.CommandExecutor import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import local.FileStorageHelper import ui.Styles -import ui.components.CheckboxWithText -import ui.components.ChooseFileTextField -import ui.components.CustomTextField +import ui.components.* import utils.* import java.awt.Desktop import java.awt.FileDialog @@ -35,7 +35,7 @@ import utils.Strings @Composable @Preview -fun App(fileStorageHelper: FileStorageHelper, savedPath: String?) { +fun App(fileStorageHelper: FileStorageHelper, savedPath: String?, adbSavedPath: String?) { val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) val coroutineScope = rememberCoroutineScope() var logs by remember { mutableStateOf("") } @@ -57,21 +57,58 @@ fun App(fileStorageHelper: FileStorageHelper, savedPath: String?) { var keyPassword by remember { mutableStateOf("") } var isAutoUnzip by remember { mutableStateOf(true) } var savedJarPath by remember { mutableStateOf(savedPath) } + var isAdbSetupDone by remember { mutableStateOf(false) } + var adbPath by remember { mutableStateOf("") } + var showLoadingDialog by remember { mutableStateOf(Pair("", false)) } - - //TODO: KNOWN ISSUE - Can't update file path once saved, For now Delete path.kb file inside storage directory. + //TODO: (Fixed this issue need to test more!) - Can't update file path once saved, For now Delete path.kb file inside storage directory. savedJarPath?.let { bundletoolPath = it saveJarPath = true } + //Check if ADB Setup is Done or Not + adbSavedPath?.let { + adbPath = it + isAdbSetupDone = true + } + if (isOpen && !isLoading) { FileDialog { fileName, directory -> isOpen = false + if (fileName.isNullOrEmpty() || directory.isNullOrEmpty()) { + return@FileDialog + } when (fileDialogType) { FileDialogType.BUNDLETOOL -> bundletoolPath = "${directory}$fileName" FileDialogType.AAPT2 -> aapt2Path = "${directory}$fileName" FileDialogType.KEY_STORE_PATH -> keyStorePath = "${directory}$fileName" + FileDialogType.ADB_PATH -> { + adbPath = "${directory}${fileName}" + //Show Loading Here + showLoadingDialog = Pair(Strings.VERIFYING_ADB_PATH, true) + CommandExecutor().executeCommand( + CommandBuilder() + .verifyAdbPath(true, adbPath) + .getAdbVerifyCommand(), coroutineScope, + onSuccess = { + logs += it + isAdbSetupDone = true + Log.i("Saving Path in DB $adbPath") + fileStorageHelper.save(DBConstants.ADB_PATH, adbPath) + //Hide Loading + Thread.sleep(1000L) + showLoadingDialog = Pair(Strings.VERIFYING_ADB_PATH, false) + }, + onFailure = { + logs += it + isAdbSetupDone = false + //Hide Loading + showLoadingDialog = Pair(Strings.VERIFYING_ADB_PATH, false) + } + ) + } + else -> { aabFilePath = Pair(directory, fileName) } @@ -79,6 +116,10 @@ fun App(fileStorageHelper: FileStorageHelper, savedPath: String?) { } } + if (showLoadingDialog.second) { + LoadingDialog(showLoadingDialog.first) + } + if (isExecute) { isExecute = false //Get Command to Execute @@ -158,13 +199,26 @@ fun App(fileStorageHelper: FileStorageHelper, savedPath: String?) { modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start ) { - Spacer(modifier = Modifier.padding(12.dp)) - Text( - text = Strings.APP_NAME, - style = Styles.TextStyleBold(28.sp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = Strings.APP_NAME, + style = Styles.TextStyleBold(28.sp), + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) + ) + ButtonWithToolTip( + if (isAdbSetupDone) Strings.ABD_SETUP_DONE else Strings.SETUP_ADB, + onClick = { + fileDialogType = FileDialogType.ADB_PATH + isOpen = true + }, + Strings.SETUP_ADB_INFO, + icon = if (isAdbSetupDone) "done" else "info", + ) + } Spacer(modifier = Modifier.padding(8.dp)) Row( horizontalArrangement = Arrangement.SpaceBetween, @@ -412,7 +466,7 @@ fun App(fileStorageHelper: FileStorageHelper, savedPath: String?) { @Composable private fun FileDialog( parent: Frame? = null, - onCloseRequest: (fileName: String, directory: String) -> Unit + onCloseRequest: (fileName: String?, directory: String?) -> Unit ) = AwtWindow( create = { object : FileDialog(parent, Strings.CHOOSE_FILE, LOAD) { @@ -431,16 +485,17 @@ private fun FileDialog( fun main() = application { val fileStorageHelper = FileStorageHelper() //Check if path for bundletool exists in local storage - val path = fileStorageHelper.read("path") as String? + val path = fileStorageHelper.read(DBConstants.BUNDLETOOL_PATH) as String? + val adbPath = fileStorageHelper.read(DBConstants.ADB_PATH) as String? Log.showLogs = true Window( onCloseRequest = ::exitApplication, state = rememberWindowState( - width = 1000.dp, height = 1000.dp, + width = 1200.dp, height = 1000.dp, position = WindowPosition(Alignment.Center) ), title = Strings.APP_NAME ) { - App(fileStorageHelper, path) + App(fileStorageHelper, path, adbPath) } } \ No newline at end of file diff --git a/src/jvmMain/kotlin/CommandBuilder.kt b/src/jvmMain/kotlin/command/CommandBuilder.kt similarity index 79% rename from src/jvmMain/kotlin/CommandBuilder.kt rename to src/jvmMain/kotlin/command/CommandBuilder.kt index a13dda8..5300748 100644 --- a/src/jvmMain/kotlin/CommandBuilder.kt +++ b/src/jvmMain/kotlin/command/CommandBuilder.kt @@ -1,3 +1,5 @@ +package command + import utils.SigningMode import utils.Utils @@ -13,6 +15,7 @@ class CommandBuilder { private var keyStorePassword: String = "" private var keyAlias: String = "" private var keyPassword: String = "" + private var adbVerifyCommandExecute = Pair(false, "") fun bundletoolPath(path: String) = apply { this.bundletoolPath = path } fun aabFilePath(path: Pair) = apply { this.aabFilePath = path } @@ -26,6 +29,16 @@ class CommandBuilder { fun keyAlias(alias: String) = apply { this.keyAlias = alias } fun keyPassword(password: String) = apply { this.keyPassword = password } + fun verifyAdbPath(value: Boolean, path: String) = apply { this.adbVerifyCommandExecute = Pair(value,path) } + + fun getAdbVerifyCommand(): String { + val (forVerify,path) = adbVerifyCommandExecute + if (forVerify){ + return "\"${path}\" version" + } + return "" + } + fun validateAndGetCommand(): Pair { if (bundletoolPath.isEmpty()) { return Pair("bundletoolPath", false) @@ -55,9 +68,15 @@ class CommandBuilder { if (isAapt2PathEnabled) { commandBuilder.append("--aapt2=\"$aapt2Path\" ") } - commandBuilder.append("--bundle=\"${aabFilePath.first}${aabFilePath.second}\" --output=\"${aabFilePath.first}${aabFilePath.second.split(".")[0]}.apks\" ") + commandBuilder.append( + "--bundle=\"${aabFilePath.first}${aabFilePath.second}\" --output=\"${aabFilePath.first}${ + aabFilePath.second.split( + "." + )[0] + }.apks\" " + ) - if (signingMode == SigningMode.RELEASE){ + if (signingMode == SigningMode.RELEASE) { commandBuilder.append("--ks=$keyStorePath --ks-pass=pass:$keyStorePassword --ks-key-alias=$keyAlias --key-pass=pass:$keyPassword ") } diff --git a/src/jvmMain/kotlin/command/CommandExecutor.kt b/src/jvmMain/kotlin/command/CommandExecutor.kt new file mode 100644 index 0000000..6657df6 --- /dev/null +++ b/src/jvmMain/kotlin/command/CommandExecutor.kt @@ -0,0 +1,55 @@ +package command + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.BufferedReader +import java.io.InputStreamReader + +class CommandExecutor { + + fun executeCommand( + cmd: String, + coroutineScope: CoroutineScope, + onSuccess: (String) -> Unit, + onFailure: (Throwable) -> Unit + ) { + coroutineScope.launch(Dispatchers.IO){ + try { + val runtime = Runtime.getRuntime() + val startTime = System.currentTimeMillis() + + val process = runtime.exec(cmd) + val outputReader = BufferedReader(InputStreamReader(process.inputStream)) + val errorReader = BufferedReader(InputStreamReader(process.errorStream)) + + var output = "" + var errorOutput = "" + + // Read command output + var line: String? + while (outputReader.readLine().also { line = it } != null) { + output += line + "\n" + } + + // Read error output + while (errorReader.readLine().also { line = it } != null) { + errorOutput += line + "\n" + } + + process.waitFor() + val endTime = System.currentTimeMillis() + if (process.exitValue() == 0) { + val executionTime = ((endTime - startTime) / 1000).toString() + "s" + onSuccess("$cmd\n$output\nCommand Executed in $executionTime") + } else { + val exception = Exception("Command execution failed: $cmd$errorOutput") + onFailure(exception) + } + } catch (e: Exception) { + onFailure(e) + } + } + } + +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt b/src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt new file mode 100644 index 0000000..dd1543f --- /dev/null +++ b/src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt @@ -0,0 +1,73 @@ +package ui.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.TooltipArea +import androidx.compose.foundation.TooltipPlacement +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.loadSvgPainter +import androidx.compose.ui.res.useResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ui.Styles + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun ButtonWithToolTip(label: String, onClick: () -> Unit, toolTipText: String = "", icon: String = "info", buttonColors: ButtonColors = ButtonDefaults.buttonColors()) { + val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) + Button( + onClick = { + onClick.invoke() + }, + colors = buttonColors, + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) + .wrapContentWidth(), + ) { + Text( + text = label, + style = Styles.TextStyleMedium(16.sp), + color = Color.White, + fontWeight = FontWeight.Medium, + ) + if (toolTipText.isNotEmpty()) { + TooltipArea( + tooltip = { + // composable tooltip content + Surface( + modifier = Modifier.shadow(4.dp), + color = Color(255, 255, 210), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = toolTipText, + color = Color.Black, + style = Styles.TextStyleMedium(12.sp), + modifier = Modifier.padding(10.dp) + ) + } + }, + delayMillis = 200, // in milliseconds + tooltipPlacement = TooltipPlacement.CursorPoint( + alignment = Alignment.BottomEnd, + offset = DpOffset.Zero // tooltip offset + ) + ) { + Icon( + painter = useResource("$icon.svg") { loadSvgPainter(it, density) }, + contentDescription = icon, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt b/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt index ed92aea..6f1c88a 100644 --- a/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt +++ b/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt @@ -54,6 +54,7 @@ fun CheckboxWithText(label: String, isChecked: Boolean, onCheckedChange: (Boolea ) { Text( text = toolTipText, + style = Styles.TextStyleMedium(12.sp), modifier = Modifier.padding(10.dp) ) diff --git a/src/jvmMain/kotlin/ui/components/LoadingDialog.kt b/src/jvmMain/kotlin/ui/components/LoadingDialog.kt new file mode 100644 index 0000000..3496623 --- /dev/null +++ b/src/jvmMain/kotlin/ui/components/LoadingDialog.kt @@ -0,0 +1,57 @@ +package ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import ui.Styles + +@Composable +fun LoadingDialog(text: String) { + Surface( + shape = RoundedCornerShape(8.dp), + elevation = 8.dp + ) { + Box( + modifier = Modifier + .padding(4.dp) + .border( + BorderStroke(1.dp, Color.LightGray), + shape = RoundedCornerShape(8.dp) + ) + ) { + Dialog( + title = text, + onCloseRequest = { }, + undecorated = false, + resizable = false, + enabled = false + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.padding(16.dp).fillMaxWidth().fillMaxHeight() + ) { + Text( + text = text, + style = Styles.TextStyleBold(20.sp), + modifier = Modifier.padding(start = 8.dp,end = 8.dp, bottom = 8.dp) + ) + CircularProgressIndicator() + } + } + } + + } + +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/TextWithIcon.kt b/src/jvmMain/kotlin/ui/components/TextWithIcon.kt new file mode 100644 index 0000000..6fe735c --- /dev/null +++ b/src/jvmMain/kotlin/ui/components/TextWithIcon.kt @@ -0,0 +1,36 @@ +package ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.loadSvgPainter +import androidx.compose.ui.res.useResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ui.Styles + +@Composable +fun TextWithIcon(label: String, onIconClick: () -> Unit) { + val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) + Row { + Text( + text = label, + style = Styles.TextStyleMedium(16.sp), + color = Color.Black, + fontWeight = FontWeight.Medium, + ) + Icon( + painter = useResource("info.svg") { loadSvgPainter(it, density) }, + contentDescription = "Info", + modifier = Modifier.padding(start = 8.dp) + .clickable { onIconClick.invoke() }, + ) + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/Constant.kt b/src/jvmMain/kotlin/utils/Constant.kt index aca1089..a2f6418 100644 --- a/src/jvmMain/kotlin/utils/Constant.kt +++ b/src/jvmMain/kotlin/utils/Constant.kt @@ -6,14 +6,20 @@ object Constant { const val BUNDLE_DOWNLOAD_LINK = "https://github.com/google/bundletool/releases" } -object FileDialogType{ +object DBConstants { + const val BUNDLETOOL_PATH = "bundletool_path" + const val ADB_PATH = "adb_path" +} + +object FileDialogType { const val AAB = -1 const val BUNDLETOOL = 1 const val AAPT2 = 2 const val KEY_STORE_PATH = 3 + const val ADB_PATH = 4 } -object SigningMode{ +object SigningMode { const val DEBUG = 1 const val RELEASE = 2 } diff --git a/src/jvmMain/kotlin/utils/Strings.kt b/src/jvmMain/kotlin/utils/Strings.kt index f752659..cee16d7 100644 --- a/src/jvmMain/kotlin/utils/Strings.kt +++ b/src/jvmMain/kotlin/utils/Strings.kt @@ -28,8 +28,14 @@ object Strings { const val KEY_PASSWORD = "Key Password" const val EXECUTE = "Execute" const val FILE_OPTIONS = "File Options" + const val DEVICE_OPTIONS = "Device Options" const val AUTO_UNZIP = "Automatically Unzip and Delete Apks File" - const val AUTO_UNZIP_INFO = "Automatically Unzip and Delete Apks File" const val CLEAR_LOGS = "Clear Logs" const val LOGS_VIEW = "Logs View" + const val SETUP_ADB = "Set up Adb" + const val ABD_SETUP_DONE = "Adb Connected" + const val SETUP_ADB_INFO = "Setup ADB path to create builds based on connected device" + const val DEVICE_ID = "Device Id" + const val DEVICE_ID_INFO = "Device Id based on the Serial Number" + const val VERIFYING_ADB_PATH = "Verifying ADB Path.." } \ No newline at end of file diff --git a/src/jvmMain/resources/done.svg b/src/jvmMain/resources/done.svg new file mode 100644 index 0000000..1655d12 --- /dev/null +++ b/src/jvmMain/resources/done.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/jvmMain/resources/info.svg b/src/jvmMain/resources/info.svg index 0f10691..e87bc17 100644 --- a/src/jvmMain/resources/info.svg +++ b/src/jvmMain/resources/info.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file