diff --git a/README.md b/README.md index 91ce13e..620676a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# AAB TO APK CONVERTER (IN DEVELOPMENT) +# Bundle Tool UI - Desktop (IN DEVELOPMENT) -AAB to APK Converter is an open-source tool built with Kotlin and Desktop Compose that allows users to convert Android App Bundles (AAB) to Android Package (APK) files using Google's [BundleTool](https://github.com/google/bundletool). +Bundle Tool UI - Desktop is an open-source tool built with Kotlin and Desktop Compose that allows users to convert Android App Bundles (AAB) to Android Package (APK) files using Google's [BundleTool](https://github.com/google/bundletool). ![ATA](https://github.com/iammohdzaki/AabToApk-Desktop/blob/main/screenshots/ata.png) diff --git a/screenshots/ata.png b/screenshots/ata.png index 942a6b8..d534a66 100644 Binary files a/screenshots/ata.png and b/screenshots/ata.png differ diff --git a/src/jvmMain/kotlin/Constant.kt b/src/jvmMain/kotlin/Constant.kt index 28c695f..3682583 100644 --- a/src/jvmMain/kotlin/Constant.kt +++ b/src/jvmMain/kotlin/Constant.kt @@ -1,17 +1,10 @@ -import java.io.File -import java.net.URLDecoder - object Constant { const val SUCCESS = 0 const val FAILURE = 1 - private const val bundleToolFileName = "bundletool-all-1.15.6.jar" - fun getCommand(): String { - if(Utils.isWindowsOS()){ - val jarFileEncodedUrl = object {}.javaClass.classLoader.getResource(bundleToolFileName) - val decodedJarPath = URLDecoder.decode(jarFileEncodedUrl.path, "UTF-8") - val bundletool = File(decodedJarPath) - return "java -jar \"${bundletool.absolutePath}\" build-apks --mode=universal --bundle=\"INPUT_FILE_PATH\" --output=\"OUTPUT_FILE_NAME.apks\"" + fun getCommand(bundletoolPath: String): String { + if (Utils.isWindowsOS()) { + return "java -jar \"${bundletoolPath}\" build-apks --mode=universal --bundle=\"INPUT_FILE_PATH\" --output=\"OUTPUT_FILE_NAME.apks\"" } return "bundletool build-apks --mode=universal --bundle=INPUT_FILE_PATH --output=OUTPUT_FILE_NAME.apks" } diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index f145912..4db77d6 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -1,67 +1,98 @@ import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.border import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.rounded.Done import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.* +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import theme.Styles +import theme.components.ChooseFileTextField +import java.awt.Desktop import java.awt.FileDialog import java.awt.Frame import java.io.BufferedReader import java.io.InputStreamReader +import java.net.URI @Composable @Preview fun App() { val coroutineScope = rememberCoroutineScope() var logs by remember { mutableStateOf("========== Logs View ==========\n\n") } + var bundletoolPath by remember { mutableStateOf("") } + var aabFilePath by remember { mutableStateOf(Pair("", "")) } var isLoading by remember { mutableStateOf(false) } var isOpen by remember { mutableStateOf(false) } - if (isOpen) { + var isExecute by remember { mutableStateOf(false) } + var isBundletool by remember { mutableStateOf(false) } + + if (isOpen && !isLoading) { FileDialog { fileName, directory -> isOpen = false - //Get Command to Execute - val cmd = Constant.getCommand().replace("INPUT_FILE_PATH", "${directory}$fileName") - .replace("OUTPUT_FILE_NAME", "${directory}${fileName.split(".")[0]}") - Log.i("Command $cmd") - logs += "Executing Command : \n$cmd\n" + if (isBundletool) { + //Handle Error for Unknown files here + bundletoolPath = "${directory}$fileName" + } else { + aabFilePath = Pair(directory, fileName) + } + } + } - coroutineScope.launch(Dispatchers.IO) { - isLoading = true - val runtime = Runtime.getRuntime() - val startTime = System.currentTimeMillis() - try { - //Launch Runtime to execute command - val process = runtime.exec(cmd) - // Read and log error output - val errorReader = BufferedReader(InputStreamReader(process.errorStream)) - Log.i("Process Error Output:") - while (errorReader.readLine().also { logs += "\n ERROR -> $it" } != null) { - isLoading = false - } - process.waitFor() - val endTime = System.currentTimeMillis() - if (process.exitValue() == 0) { - Log.i("Command Executed in ${((endTime - startTime) / 1000)}s") - logs += "\nCommand Executed in ${((endTime - startTime) / 1000)}s\n" - // Do further file operation after new apks is generated - FileHelper.performFileOperations(directory, fileName) { status, message -> - isLoading = false - Log.i("STATUS - $status\nMESSAGE - $message") - logs += "\nFiles Operations Starting...\n$message\n" - } - } - } catch (e: Exception) { + if (isExecute) { + isExecute = false + //Get Command to Execute + val cmd = + Constant.getCommand(bundletoolPath).replace("INPUT_FILE_PATH", "${aabFilePath.first}${aabFilePath.second}") + .replace("OUTPUT_FILE_NAME", "${aabFilePath.first}${aabFilePath.second}".split(".")[0]) + Log.i("Command $cmd") + logs += "Executing Command : \n$cmd\n" + + coroutineScope.launch(Dispatchers.IO) { + isLoading = true + val runtime = Runtime.getRuntime() + val startTime = System.currentTimeMillis() + try { + //Launch Runtime to execute command + val process = runtime.exec(cmd) + // Read and log error output + val errorReader = BufferedReader(InputStreamReader(process.errorStream)) + Log.i("Process Error Output:") + while (errorReader.readLine().also { logs += "\n ERROR -> $it" } != null) { isLoading = false - logs += "Failed -> ${e.printStackTrace()}" } + process.waitFor() + val endTime = System.currentTimeMillis() + if (process.exitValue() == 0) { + Log.i("Command Executed in ${((endTime - startTime) / 1000)}s") + logs += "\nCommand Executed in ${((endTime - startTime) / 1000)}s\n" + // Do further file operation after new apks is generated + FileHelper.performFileOperations(aabFilePath.first, aabFilePath.second) { status, message -> + isLoading = false + Log.i("STATUS - $status\nMESSAGE - $message") + logs += "\nFiles Operations Starting...\n$message\n" + } + } + } catch (e: Exception) { + isLoading = false + logs += "Failed -> ${e.printStackTrace()}" } } } @@ -69,29 +100,78 @@ fun App() { MaterialTheme { Column( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.Start ) { + Spacer(modifier = Modifier.padding(12.dp)) Text( - text = "Aab To Apk", - style = Styles.TextStyleBold(28.sp) + text = "Android Bundletool UI", + style = Styles.TextStyleBold(28.sp), + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) + ) + Spacer(modifier = Modifier.padding(8.dp)) + //Bundle tool select flow + ChooseFileTextField( + bundletoolPath, + "Select Bundletool Jar", + onSelect = { + isOpen = true + isBundletool = true + } + ) + val downloadInfo = buildAnnotatedString { + withStyle(style = SpanStyle(color = Color.Blue,textDecoration = TextDecoration.Underline)) { + append("Download Bundletool from here") + } + addStringAnnotation( + tag = "URL", + annotation = "https://github.com/google/bundletool/releases", + start = 0, + end = length + ) + } + ClickableText( + text = downloadInfo, + style = Styles.TextStyleNormal(14.sp), + modifier = Modifier.padding(start = 16.dp), + onClick = {offset -> + val annotations = downloadInfo.getStringAnnotations("URL", offset, offset) + if (annotations.isNotEmpty()) { + val uri = URI(annotations.first().item) + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(uri) + } else { + // Desktop not supported, handle as necessary + } + } + } + ) + Spacer(modifier = Modifier.padding(8.dp)) + ChooseFileTextField( + "${aabFilePath.first}${aabFilePath.second}", + "Select Aab File", + onSelect = { + isOpen = true + isBundletool = false + } ) Spacer(modifier = Modifier.padding(8.dp)) if (isLoading) { CircularProgressIndicator( - modifier = Modifier.size(size = 30.dp), + modifier = Modifier.size(size = 40.dp).padding(start = 16.dp, top = 0.dp, end = 0.dp, bottom = 0.dp), strokeWidth = 4.dp ) } else { Button( onClick = { - isOpen = true + isLoading = true + isExecute = true }, modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - .fillMaxWidth(0.32f), + .wrapContentWidth(), ) { Text( - text = "Choose File", + text = "Execute", style = Styles.TextStyleMedium(16.sp), color = Color.White, fontWeight = FontWeight.Medium, @@ -133,11 +213,11 @@ fun main() = application { Window( onCloseRequest = ::exitApplication, state = rememberWindowState( - width = 500.dp, height = 500.dp, + width = 800.dp, height = 800.dp, position = WindowPosition(Alignment.Center) ), title = "Aab To Apk" ) { App() } -} +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/theme/components/ChooseFileTextField.kt b/src/jvmMain/kotlin/theme/components/ChooseFileTextField.kt new file mode 100644 index 0000000..bf67443 --- /dev/null +++ b/src/jvmMain/kotlin/theme/components/ChooseFileTextField.kt @@ -0,0 +1,65 @@ +package theme.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.* +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.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 theme.Styles + +@Composable +fun ChooseFileTextField(value: String, label: String, onSelect: () -> Unit) { + val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) + Row( + modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 0.dp, bottom = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + modifier = Modifier.fillMaxWidth(0.5f).height(60.dp) + .border( + BorderStroke(width = 2.dp, color = MaterialTheme.colors.primary), + shape = RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp, topStart = 10.dp, bottomStart = 10.dp) + ), + value = value, + singleLine = true, + readOnly = true, + colors = TextFieldDefaults.textFieldColors( + backgroundColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent + ), + label = { + Text( + text = label, + style = Styles.TextStyleMedium(16.sp), + fontWeight = FontWeight.Medium, + ) + }, + textStyle = Styles.TextStyleMedium(16.sp), + onValueChange = {}, + ) + Button( + onClick = { + onSelect.invoke() + }, + modifier = Modifier + .height(60.dp) + .wrapContentWidth(), + ) { + Icon( + painter = useResource("open_folder.svg") { loadSvgPainter(it, density) }, + contentDescription = "" + ) + } + } +} \ No newline at end of file diff --git a/src/jvmMain/resources/bundletool-all-1.15.6.jar b/src/jvmMain/resources/bundletool-all-1.15.6.jar deleted file mode 100644 index 6d7f4a2..0000000 Binary files a/src/jvmMain/resources/bundletool-all-1.15.6.jar and /dev/null differ diff --git a/src/jvmMain/resources/open_folder.svg b/src/jvmMain/resources/open_folder.svg new file mode 100644 index 0000000..65b7a9f --- /dev/null +++ b/src/jvmMain/resources/open_folder.svg @@ -0,0 +1 @@ + \ No newline at end of file