diff --git a/.github/workflows/bundletool-native.yml b/.github/workflows/bundletool-native.yml
new file mode 100644
index 0000000..ecc8b1c
--- /dev/null
+++ b/.github/workflows/bundletool-native.yml
@@ -0,0 +1,51 @@
+name: Build Native Bundletool ARM64
+
+on:
+ workflow_dispatch:
+
+jobs:
+ build-native:
+ runs-on: ubuntu-22.04
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Install GraalVM (Java 21)
+ uses: graalvm/setup-graalvm@v1
+ with:
+ version: "21.0.2"
+ java-version: "21"
+ components: "native-image"
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y unzip zip curl clang build-essential
+
+ - name: Download bundletool source
+ run: |
+ curl -L -o bundletool.zip https://github.com/google/bundletool/archive/refs/heads/master.zip
+ unzip bundletool.zip
+ mv bundletool-master bundletool-src
+
+ - name: Build bundletool.jar
+ working-directory: bundletool-src
+ run: |
+ ./gradlew build -x test
+ cp build/libs/bundletool-*.jar ../bundletool.jar
+
+ - name: Build Native Image (ARM64)
+ run: |
+ native-image \
+ --no-fallback \
+ --static \
+ -jar bundletool.jar \
+ bundletool
+
+ - name: Upload Native Binary
+ uses: actions/upload-artifact@v4
+ with:
+ name: bundletool-native-arm64
+ path: bundletool
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index b63da45..ff820a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,42 +1,19 @@
-.gradle
+# Dependencies
+node_modules/
+
+# Build outputs
+dist/
build/
-!gradle/wrapper/gradle-wrapper.jar
-!**/src/main/**/build/
-!**/src/test/**/build/
+*.log
-### IntelliJ IDEA ###
-.idea/modules.xml
-.idea/jarRepositories.xml
-.idea/compiler.xml
-.idea/libraries/
-*.iws
-*.iml
-*.ipr
-out/
-!**/src/main/**/out/
-!**/src/test/**/out/
-
-### Eclipse ###
-.apt_generated
-.classpath
-.factorypath
-.project
-.settings
-.springBeans
-.sts4-cache
-bin/
-!**/src/main/**/bin/
-!**/src/test/**/bin/
-
-### NetBeans ###
-/nbproject/private/
-/nbbuild/
-/dist/
-/nbdist/
-/.nb-gradle/
-
-### VS Code ###
+# IDE
.vscode/
+.idea/
-### Mac OS ###
-.DS_Store
\ No newline at end of file
+# OS
+.DS_Store
+Thumbs.db
+
+### Markdown files ###
+*.md
+!README.md
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..e5f4346
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,42 @@
+plugins {
+ id("com.android.application")
+ kotlin("android")
+}
+
+android {
+ namespace = "com.aab2apk"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "com.aab2apk"
+ minSdk = 23
+ targetSdk = 36
+ versionCode = 1
+ versionName = "1.0"
+ ndkVersion = "28.2.13676358"
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = "17"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
+ }
+ }
+}
+
+dependencies {
+ implementation("androidx.core:core-ktx:1.10.1")
+ implementation("androidx.appcompat:appcompat:1.6.1")
+ implementation("com.google.android.material:material:1.9.0")
+ implementation("androidx.activity:activity-compose:1.7.2")
+ implementation("androidx.compose.material3:material3:1.1.1")
+}
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
new file mode 100644
index 0000000..ae17938
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1 @@
+# proguard rules
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..09c19f1
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/README-bundletool.txt b/app/src/main/assets/README-bundletool.txt
new file mode 100644
index 0000000..2ffc71d
--- /dev/null
+++ b/app/src/main/assets/README-bundletool.txt
@@ -0,0 +1,12 @@
+This is a placeholder asset named 'bundletool'.
+
+To enable on-device AAB -> APK conversion you must replace this file with a native bundletool executable
+built for Android (ARM64 or ARMv7) using GraalVM native-image or a prebuilt binary.
+
+Recommended approach:
+1. Build bundletool native image with GraalVM (supports linux/arm64 target) or obtain a prebuilt binary.
+2. Rename the binary to 'bundletool' and place it here: app/src/main/assets/bundletool
+3. Rebuild the app. On first run the app will copy the asset to its internal files directory and mark it executable.
+4. The UI will then be able to run bundletool to convert AAB -> APKS.
+
+If you want, I can help produce a GraalVM build script for bundletool or attempt to include a tested ARM64 binary.
diff --git a/app/src/main/assets/bundletool b/app/src/main/assets/bundletool
new file mode 100644
index 0000000..42df81b
--- /dev/null
+++ b/app/src/main/assets/bundletool
@@ -0,0 +1,2 @@
+# Placeholder bundletool binary
+# REPLACE THIS FILE with a native ARM64 bundletool executable built with GraalVM.
diff --git a/app/src/main/java/com/aab2apk/AabToApkManager.kt b/app/src/main/java/com/aab2apk/AabToApkManager.kt
new file mode 100644
index 0000000..feafcbe
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/AabToApkManager.kt
@@ -0,0 +1,103 @@
+package com.aab2apk
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import android.widget.Toast
+import androidx.documentfile.provider.DocumentFile
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import java.io.File
+import java.io.InputStream
+import java.io.OutputStream
+
+object AabToApkManager {
+ private const val TAG = "AabToApkManager"
+
+ /**
+ * Attempt to convert an AAB to APK(s).
+ *
+ * This is a best-effort adapter: real conversion normally requires bundletool (Java jar) or
+ * server-side conversion. On-device conversion is not generally supported unless you include
+ * an executable bundletool binary compiled for Android and place it in the app's files dir.
+ *
+ * This method will:
+ * - copy the selected AAB to the app's internal files directory
+ * - check for a self-contained 'bundletool' executable in filesDir and try to run it (if present)
+ * - otherwise, show a Toast explaining how to perform conversion externally (or how to provide bundletool)
+ */
+ suspend fun convertAab(
+ context: Context,
+ aabUri: Uri,
+ outputDirUri: Uri?
+ ): Result = withContext(Dispatchers.IO) {
+ try {
+ // Copy AAB to internal storage
+ val aabFile = File(context.filesDir, "input.aab")
+ context.contentResolver.openInputStream(aabUri).use { input ->
+ if (input == null) return@withContext Result.failure(Exception("Cannot open AAB input"))
+ aabFile.outputStream().use { output ->
+ input.copyTo(output)
+ }
+ }
+
+ // Ensure a bundletool asset (if packaged) is installed to filesDir and set executable
+ val bundletoolFile = File(context.filesDir, "bundletool")
+ try {
+ if (!bundletoolFile.exists()) {
+ // Attempt to copy bundled asset (if present) into filesDir
+ context.assets.open("bundletool").use { input ->
+ bundletoolFile.outputStream().use { output -> input.copyTo(output) }
+ }
+ bundletoolFile.setExecutable(true)
+ }
+ } catch (e: Exception) {
+ // If asset not present or copy failed, we'll proceed to check for an existing executable.
+ }
+ if (bundletoolFile.exists() && bundletoolFile.canExecute()) {
+ // Build output path
+ val outFile = File(context.filesDir, "output.apks")
+ val cmd = arrayListOf(bundletoolFile.absolutePath, "build-apks", "--bundle=${aabFile.absolutePath}", "--output=${outFile.absolutePath}", "--mode=universal")
+ Log.i(TAG, "Running: ${cmd.joinToString(" ")}")
+ val procBuilder = ProcessBuilder(cmd)
+ procBuilder.redirectErrorStream(true)
+ val proc = procBuilder.start()
+ val stdout = StringBuilder()
+ proc.inputStream.bufferedReader().useLines { lines ->
+ lines.forEach { stdout.append(it).append("\n") }
+ }
+ val exit = proc.waitFor()
+ if (exit == 0) {
+ // If outputDirUri provided, copy the result there
+ if (outputDirUri != null) {
+ val doc = DocumentFile.fromTreeUri(context, outputDirUri)
+ if (doc != null && doc.canWrite()) {
+ val outDoc = doc.createFile("application/octet-stream", "output.apks")
+ if (outDoc != null) {
+ context.contentResolver.openOutputStream(outDoc.uri).use { os ->
+ outFile.inputStream().use { fis -> fis.copyTo(os!!) }
+ }
+ return@withContext Result.success("Conversion complete — output.apks saved to destination folder.")
+ }
+ }
+ }
+ return@withContext Result.success("Conversion complete. Output at ${outFile.absolutePath}")
+ } else {
+ return@withContext Result.failure(Exception("bundletool failed with exit $exit\n${stdout}"))
+ }
+ } else {
+ // No bundletool available — inform user what to do
+ val msg = """bundletool executable not found in app files.
+To convert an AAB to APK on-device you must provide a bundletool binary built for Android and place it at: ${context.filesDir}/bundletool
+Alternatively, convert the AAB on a desktop using bundletool.jar:
+java -jar bundletool-all.jar build-apks --bundle=app.aab --output=app.apks --mode=universal
+Then transfer resulting .apks/.apks.zip to the phone."""
+ Log.i(TAG, msg)
+ return@withContext Result.failure(Exception(msg))
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Conversion failed", e)
+ return@withContext Result.failure(e)
+ }
+ }
+}
diff --git a/app/src/main/java/com/aab2apk/Main.kt b/app/src/main/java/com/aab2apk/Main.kt
new file mode 100644
index 0000000..693b705
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/Main.kt
@@ -0,0 +1,575 @@
+package com.aab2apk.original
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.text.ClickableText
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+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.painterResource
+import androidx.compose.ui.res.useResource
+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.AwtWindow
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.WindowPosition
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import command.CommandBuilder
+import command.CommandExecutor
+import local.FileStorageHelper
+import ui.Styles
+import ui.components.ButtonWithToolTip
+import ui.components.CheckboxWithText
+import ui.components.ChooseFileTextField
+import ui.components.CustomTextField
+import ui.components.LoadingDialog
+import utils.Constant
+import utils.DBConstants
+import utils.FileDialogType
+import utils.FileHelper
+import utils.Log
+import utils.SigningMode
+import utils.Strings
+import java.awt.Desktop
+import java.awt.FileDialog
+import java.awt.Frame
+import java.net.URI
+
+@Composable
+@Preview
+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("") }
+ var bundletoolPath by remember { mutableStateOf("") }
+ var aabFilePath by remember { mutableStateOf(Pair("", "")) }
+ var isLoading by remember { mutableStateOf(false) }
+ var isOpen by remember { mutableStateOf(false) }
+ var isExecute by remember { mutableStateOf(false) }
+ var fileDialogType by remember { mutableStateOf(0) }
+ var saveJarPath by remember { mutableStateOf(false) }
+ var isOverwrite by remember { mutableStateOf(false) }
+ var isAapt2PathEnabled by remember { mutableStateOf(false) }
+ var aapt2Path by remember { mutableStateOf("") }
+ var isUniversalMode by remember { mutableStateOf(true) }
+ var signingMode by remember { mutableStateOf(SigningMode.DEBUG) }
+ var keyStorePath by remember { mutableStateOf("") }
+ var keyStorePassword by remember { mutableStateOf("") }
+ var keyAlias by remember { mutableStateOf("") }
+ 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)) }
+ var isDeviceIdEnabled by remember { mutableStateOf(false) }
+ var deviceSerialId by remember { mutableStateOf("") }
+
+ // 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)
+ }
+ }
+ }
+ }
+
+ if (showLoadingDialog.second) {
+ LoadingDialog(showLoadingDialog.first)
+ }
+
+ if (isExecute) {
+ isExecute = false
+ // Get Command to Execute
+ val (cmd, isValid) = CommandBuilder()
+ .bundletoolPath(bundletoolPath)
+ .aabFilePath(aabFilePath)
+ .isOverwrite(isOverwrite)
+ .isUniversalMode(isUniversalMode)
+ .isAapt2PathEnabled(isAapt2PathEnabled)
+ .aapt2Path(aapt2Path)
+ .signingMode(signingMode)
+ .keyStorePath(keyStorePath)
+ .keyStorePassword(keyStorePassword)
+ .keyAlias(keyAlias)
+ .keyPassword(keyPassword)
+ .validateAndGetCommand()
+ if (isValid) {
+ Log.i("Command $cmd")
+ // logs += "Executing Command : \n$cmd\n"
+ CommandExecutor()
+ .executeCommand(
+ cmd,
+ coroutineScope,
+ onSuccess = {
+ logs += "$it\n"
+ // Do further file operation after new apks is generated
+ // From Auto Zip you can control further file operations.
+ if (isAutoUnzip) {
+ FileHelper.performFileOperations(aabFilePath.first, aabFilePath.second) { status, message ->
+ isLoading = false
+ Log.i("STATUS - $status\nMESSAGE - $message")
+ logs += "\nFiles Operations Starting...\n$message\n"
+ }
+ } else {
+ logs += "\nFile will be saved at ${aabFilePath.first.removeSuffix("\\")}.\n"
+ isLoading = false
+ }
+
+ // Save Path in Storage
+ // If We don't have any saved path in file storage and save jar path option is checked.Then,we can save new value in storage.
+ if (savedJarPath == null && saveJarPath) {
+ fileStorageHelper.save("path", bundletoolPath)
+ }
+ // If we have saved path in file storage, and it is not changed and save jar path is checked. Then, we can update the value in file storage.
+ else if (savedJarPath != null && savedJarPath != bundletoolPath && saveJarPath) {
+ fileStorageHelper.save("path", bundletoolPath)
+ } else {
+ if (fileStorageHelper.delete("path"))
+ saveJarPath = false
+ }
+ },
+ onFailure = {
+ isLoading = false
+ logs += "Failed -> ${it.printStackTrace()}"
+ }
+ )
+ } else {
+ Log.i("Error $cmd")
+ logs += "\nError -> $cmd"
+ isLoading = false
+ }
+ }
+
+ MaterialTheme {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.Start
+ ) {
+ Spacer(modifier = Modifier.padding(12.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,
+ modifier = Modifier.wrapContentSize(),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ // Bundle tool select flow
+ ChooseFileTextField(
+ bundletoolPath,
+ Strings.SELECT_BUNDLETOOL_JAR,
+ onSelect = {
+ fileDialogType = FileDialogType.BUNDLETOOL
+ isOpen = true
+ }
+ )
+ CheckboxWithText(
+ Strings.SAVE_JAR_PATH,
+ saveJarPath,
+ onCheckedChange = {
+ saveJarPath = it
+ },
+ Strings.SAVE_JAR_PATH_INFO
+ )
+ }
+ val downloadInfo = buildAnnotatedString {
+ withStyle(style = SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)) {
+ append(Strings.DOWNLOAD_BUNDLETOOL)
+ }
+ addStringAnnotation(
+ tag = Strings.URL,
+ annotation = Constant.BUNDLE_DOWNLOAD_LINK,
+ start = 0,
+ end = length
+ )
+ }
+ ClickableText(
+ text = downloadInfo,
+ style = Styles.TextStyleMedium(14.sp),
+ modifier = Modifier.padding(start = 16.dp),
+ onClick = { offset ->
+ val annotations = downloadInfo.getStringAnnotations(Strings.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}",
+ Strings.SELECT_AAB_FILE,
+ onSelect = {
+ fileDialogType = FileDialogType.AAB
+ isOpen = true
+ }
+ )
+ Spacer(modifier = Modifier.padding(8.dp))
+ Text(
+ text = Strings.OPTIONS_FOR_BUILD_APKS,
+ style = Styles.TextStyleBold(20.sp),
+ modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ CheckboxWithText(
+ Strings.OVERWRITE,
+ isOverwrite,
+ onCheckedChange = {
+ isOverwrite = it
+ },
+ Strings.OVERWRITE_INFO
+ )
+ CheckboxWithText(
+ Strings.MODE_UNIVERSAL,
+ isUniversalMode,
+ onCheckedChange = {
+ isUniversalMode = it
+ },
+ Strings.MODE_UNIVERSAL_INFO
+ )
+ CheckboxWithText(
+ Strings.AAPT2_PATH,
+ isAapt2PathEnabled,
+ onCheckedChange = {
+ isAapt2PathEnabled = it
+ },
+ Strings.AAPT2_PATH_INFO
+ )
+ }
+ if (isAapt2PathEnabled) {
+ ChooseFileTextField(
+ aapt2Path,
+ Strings.SELECT_AAPT2_FILE,
+ onSelect = {
+ fileDialogType = FileDialogType.AAPT2
+ isOpen = true
+ }
+ )
+ }
+ Text(
+ text = Strings.SIGNING_MODE,
+ style = Styles.TextStyleBold(16.sp),
+ modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ CheckboxWithText(
+ Strings.DEBUG,
+ signingMode == SigningMode.DEBUG,
+ onCheckedChange = {
+ signingMode = if (it) SigningMode.DEBUG
+ else SigningMode.RELEASE
+ }
+ )
+ CheckboxWithText(
+ Strings.RELEASE,
+ signingMode == SigningMode.RELEASE,
+ onCheckedChange = {
+ signingMode = if (it) SigningMode.RELEASE
+ else SigningMode.DEBUG
+ }
+ )
+ }
+ if (signingMode == SigningMode.RELEASE) {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ ChooseFileTextField(
+ keyStorePath,
+ Strings.KEYSTORE_PATH,
+ onSelect = {
+ fileDialogType = FileDialogType.KEY_STORE_PATH
+ isOpen = true
+ }
+ )
+ CustomTextField(
+ keyStorePassword,
+ Strings.KEYSTORE_PASSWORD,
+ onValueChange = {
+ keyStorePassword = it
+ }
+ )
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ CustomTextField(
+ keyAlias,
+ Strings.KEY_ALIAS,
+ forPassword = false,
+ onValueChange = {
+ keyAlias = it
+ }
+ )
+ CustomTextField(
+ keyPassword,
+ Strings.KEY_PASSWORD,
+ onValueChange = {
+ keyPassword = it
+ }
+ )
+ }
+ }
+ Text(
+ text = Strings.FILE_OPTIONS,
+ style = Styles.TextStyleBold(16.sp),
+ modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
+ )
+ CheckboxWithText(
+ Strings.AUTO_UNZIP,
+ isAutoUnzip,
+ onCheckedChange = {
+ isAutoUnzip = it
+ },
+ Strings.AUTO_UNZIP
+ )
+ Spacer(modifier = Modifier.padding(8.dp))
+ if (isAdbSetupDone) {
+ Text(
+ text = Strings.DEVICE_OPTIONS,
+ style = Styles.TextStyleBold(16.sp),
+ modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
+ )
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ CheckboxWithText(
+ Strings.DEVICE_ID,
+ isDeviceIdEnabled,
+ onCheckedChange = {
+ isDeviceIdEnabled = it
+ },
+ Strings.DEVICE_ID_INFO
+ )
+ if (isDeviceIdEnabled) {
+ CustomTextField(
+ deviceSerialId,
+ Strings.SERIAL_ID,
+ forPassword = false,
+ onValueChange = {
+ deviceSerialId = it
+ }
+ )
+ ButtonWithToolTip(
+ Strings.FETCH_DEVICES,
+ onClick = {
+ CommandExecutor()
+ .executeCommand(
+ CommandBuilder()
+ .getAdbFetchCommand(adbSavedPath!!),
+ coroutineScope,
+ onSuccess = {
+ logs += it
+ },
+ onFailure = {
+ logs += it.printStackTrace()
+ }
+ )
+ },
+ Strings.FETCH_DEVICES_INFO,
+ icon = "device_fetch"
+ )
+ }
+ }
+ Spacer(modifier = Modifier.padding(8.dp))
+ }
+ if (isLoading) {
+ CircularProgressIndicator(
+ 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 = {
+ isLoading = true
+ isExecute = true
+ },
+ modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
+ .wrapContentWidth()
+ ) {
+ Text(
+ text = Strings.EXECUTE,
+ style = Styles.TextStyleMedium(16.sp),
+ color = Color.White,
+ fontWeight = FontWeight.Medium
+ )
+ }
+ }
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.Bottom
+ ) {
+ Text(
+ text = Strings.LOGS_VIEW,
+ style = Styles.TextStyleBold(20.sp),
+ modifier = Modifier.padding(start = 0.dp, end = 0.dp, bottom = 8.dp)
+ )
+ Button(
+ modifier = Modifier.padding(end = 0.dp, bottom = 8.dp),
+ onClick = {
+ logs = ""
+ }
+ ) {
+ Text(
+ text = Strings.CLEAR_LOGS,
+ style = Styles.TextStyleBold(13.sp)
+ )
+ Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing))
+ Icon(
+ painter = useResource("clear.svg") { loadSvgPainter(it, density) },
+ contentDescription = "Clear",
+ modifier = Modifier.size(ButtonDefaults.IconSize)
+ )
+ }
+ }
+ TextField(
+ modifier = Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+ value = logs,
+ textStyle = Styles.TextStyleMedium(16.sp),
+ onValueChange = {}
+ )
+ }
+ }
+}
+
+@Composable
+private fun FileDialog(
+ parent: Frame? = null,
+ onCloseRequest: (fileName: String?, directory: String?) -> Unit
+) = AwtWindow(
+ create = {
+ object : FileDialog(parent, Strings.CHOOSE_FILE, LOAD) {
+ override fun setVisible(value: Boolean) {
+ super.setVisible(value)
+ if (value) {
+ onCloseRequest(file, directory)
+ }
+ }
+ }
+ },
+ dispose = FileDialog::dispose
+)
+
+fun main() = application {
+ val fileStorageHelper = FileStorageHelper()
+ // Check if path for bundletool exists in local storage
+ val path = fileStorageHelper.read(DBConstants.BUNDLETOOL_PATH) as String?
+ val adbPath = fileStorageHelper.read(DBConstants.ADB_PATH) as String?
+ val icon = painterResource("launcher.png")
+ Log.showLogs = true
+ Window(
+ icon = icon,
+ onCloseRequest = ::exitApplication,
+ state = rememberWindowState(
+ width = 1200.dp, height = 1000.dp,
+ position = WindowPosition(Alignment.Center)
+ ),
+ title = Strings.APP_NAME
+ ) {
+ App(fileStorageHelper, path, adbPath)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/MainActivity.kt b/app/src/main/java/com/aab2apk/MainActivity.kt
new file mode 100644
index 0000000..fe53c7c
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/MainActivity.kt
@@ -0,0 +1,79 @@
+package com.aab2apk
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.widget.Toast
+import androidx.activity.ComponentActivity
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+class MainActivity: ComponentActivity() {
+ private var pickedAab: Uri? = null
+ private var pickedOutputFolder: Uri? = null
+
+ private val pickAabLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
+ if (uri != null) {
+ contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ pickedAab = uri
+ Toast.makeText(this, "AAB selected", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ private val pickFolderLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
+ if (uri != null) {
+ contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ pickedOutputFolder = uri
+ Toast.makeText(this, "Output folder selected", Toast.LENGTH_SHORT).show()
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
+ Column(Modifier.padding(16.dp)) {
+ Text(text = "aab2apk", style = MaterialTheme.typography.headlineSmall)
+ Button(onClick = { pickAabLauncher.launch(arrayOf("application/octet-stream","*/*")) }, modifier = Modifier.padding(top = 12.dp)) {
+ Text("Pick .aab file")
+ }
+ Button(onClick = { pickFolderLauncher.launch(null) }, modifier = Modifier.padding(top = 12.dp)) {
+ Text("Select output folder")
+ }
+ Button(onClick = {
+ if (pickedAab == null) {
+ Toast.makeText(this, "Please pick an AAB first", Toast.LENGTH_SHORT).show()
+ return@Button
+ }
+ CoroutineScope(Dispatchers.Main).launch {
+ Toast.makeText(this@MainActivity, "Starting conversion (best-effort)...", Toast.LENGTH_SHORT).show()
+ val result = AabToApkManager.convertAab(this@MainActivity, pickedAab!!, pickedOutputFolder)
+ if (result.isSuccess) {
+ Toast.makeText(this@MainActivity, "Success: ${'$'}{result.getOrNull()}", Toast.LENGTH_LONG).show()
+ } else {
+ Toast.makeText(this@MainActivity, "Failed: ${'$'}{result.exceptionOrNull()?.message}", Toast.LENGTH_LONG).show()
+ }
+ }
+ }, modifier = Modifier.padding(top = 12.dp)) {
+ Text("Convert AAB to APK (best-effort)")
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/aab2apk/command/CommandBuilder.kt b/app/src/main/java/com/aab2apk/command/CommandBuilder.kt
new file mode 100644
index 0000000..426b6cf
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/command/CommandBuilder.kt
@@ -0,0 +1,119 @@
+package com.aab2apk.original
+
+
+import utils.Log
+import utils.SigningMode
+import utils.Utils
+
+class CommandBuilder {
+ private var bundletoolPath: String = ""
+ private var aabFilePath: Pair = Pair("", "")
+ private var isOverwrite: Boolean = false
+ private var isAapt2PathEnabled: Boolean = false
+ private var aapt2Path: String = ""
+ private var isUniversalMode: Boolean = true
+ private var signingMode: Int = 1
+ private var keyStorePath: String = ""
+ private var keyStorePassword: String = ""
+ private var keyAlias: String = ""
+ private var keyPassword: String = ""
+ private var adbVerifyCommandExecute = Pair(false, "")
+ private var isDeviceIdEnabled: Boolean = false
+ private var adbSerialId: String = ""
+
+ fun bundletoolPath(path: String) = apply { this.bundletoolPath = path }
+ fun aabFilePath(path: Pair) = apply { this.aabFilePath = path }
+ fun isOverwrite(overwrite: Boolean) = apply { this.isOverwrite = overwrite }
+ fun isAapt2PathEnabled(enabled: Boolean) = apply { this.isAapt2PathEnabled = enabled }
+ fun aapt2Path(path: String) = apply { this.aapt2Path = path }
+ fun isUniversalMode(universalMode: Boolean) = apply { this.isUniversalMode = universalMode }
+ fun signingMode(mode: Int) = apply { this.signingMode = mode }
+ fun keyStorePath(path: String) = apply { this.keyStorePath = path }
+ fun keyStorePassword(password: String) = apply { this.keyStorePassword = password }
+ 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 isDeviceSerialIdEnabled(value: Boolean) = apply { this.isDeviceIdEnabled = value }
+
+ fun adbSerialId(value: String) = apply { this.adbSerialId = value }
+
+ fun getAdbVerifyCommand(): String {
+ val (forVerify, path) = adbVerifyCommandExecute
+ if (forVerify) {
+ return if (Utils.isWindowsOS()) "\"${path}\" version" else "$path version"
+ }
+ return ""
+ }
+
+ fun getAdbFetchCommand(adbPath: String): String {
+ return if (Utils.isWindowsOS()) "\"${adbPath}\" devices" else "$adbPath devices"
+ }
+
+ fun validateAndGetCommand(): Pair {
+ if (bundletoolPath.isEmpty()) {
+ return Pair("bundletoolPath", false)
+ }
+ if (aabFilePath.first.isEmpty() || aabFilePath.second.isEmpty()) {
+ return Pair("aabFilePath", false)
+ }
+ if (isAapt2PathEnabled && aapt2Path.isEmpty()) {
+ return Pair("aapt2Path", false)
+ }
+ if (signingMode == SigningMode.RELEASE && (keyStorePath.isEmpty() || keyStorePassword.isEmpty() || keyAlias.isEmpty() || keyPassword.isEmpty())) {
+ return Pair("Check Keystore Info!", false)
+ }
+ if (isDeviceIdEnabled && adbSerialId.isEmpty()) {
+ return Pair("Invalid Serial ID", false)
+ }
+ return Pair(getCommand(), true)
+ }
+
+ private fun getCommand(): String {
+ val commandBuilder = StringBuilder()
+ if (Utils.isWindowsOS()) {
+ commandBuilder.append("java -jar \"$bundletoolPath\" build-apks ")
+ if (isAapt2PathEnabled) {
+ commandBuilder.append("--aapt2=\"$aapt2Path\" ")
+ }
+ commandBuilder.append(
+ "--bundle=\"${aabFilePath.first}${aabFilePath.second}\" --output=\"${aabFilePath.first}${
+ aabFilePath.second.split(
+ "."
+ )[0]
+ }.apks\" "
+ )
+ } else {
+ commandBuilder.append("java -jar $bundletoolPath build-apks ")
+ if (isAapt2PathEnabled) {
+ commandBuilder.append("--aapt2=$aapt2Path ")
+ }
+ commandBuilder.append(
+ "--bundle=${aabFilePath.first}${aabFilePath.second} --output=${aabFilePath.first}${
+ aabFilePath.second.split(
+ "."
+ )[0]
+ }.apks "
+ )
+ }
+
+ if (isUniversalMode) {
+ commandBuilder.append("--mode=universal ")
+ }
+ if (isOverwrite) {
+ commandBuilder.append("--overwrite ")
+ }
+
+ if (signingMode == SigningMode.RELEASE) {
+ commandBuilder.append("--ks=$keyStorePath --ks-pass=pass:$keyStorePassword --ks-key-alias=$keyAlias --key-pass=pass:$keyPassword ")
+ }
+
+ if (isDeviceIdEnabled) {
+ commandBuilder.append("--device-id=$adbSerialId ")
+ }
+
+ Log.i("COMMAND_BUILDER -> $commandBuilder")
+ return commandBuilder.toString()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/command/CommandExecutor.kt b/app/src/main/java/com/aab2apk/command/CommandExecutor.kt
new file mode 100644
index 0000000..871b386
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/command/CommandExecutor.kt
@@ -0,0 +1,55 @@
+package com.aab2apk.original
+
+
+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/app/src/main/java/com/aab2apk/local/FileStorageHelper.kt b/app/src/main/java/com/aab2apk/local/FileStorageHelper.kt
new file mode 100644
index 0000000..7c29efa
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/local/FileStorageHelper.kt
@@ -0,0 +1,76 @@
+package com.aab2apk.original
+
+
+import com.esotericsoftware.kryo.Kryo
+import com.esotericsoftware.kryo.io.Input
+import com.esotericsoftware.kryo.io.Output
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.nio.file.Paths
+
+const val DB_PATH = "/storage"
+
+class FileStorageHelper {
+
+ private lateinit var kryo: Kryo
+
+ init {
+ initializeDir()
+ }
+
+ private fun getKryo(): Kryo {
+ return if (this::kryo.isInitialized) {
+ kryo
+ } else {
+ val kryo = Kryo()
+ kryo.register(KiteTable::class.java)
+ kryo
+ }
+ }
+
+ private fun initializeDir() {
+ val file = File(getCurrentDir())
+ file.mkdir()
+ }
+
+ private fun getCurrentDir(): String {
+ return Paths.get("").toAbsolutePath().toString() + DB_PATH
+ }
+
+ private fun getPath(key: String): String {
+ return "${getCurrentDir()}/$key.kb"
+ }
+
+ fun save(key: String, value: E) {
+ var kryoOutput: Output?
+ try {
+ val kiteTable = KiteTable(value)
+ val fileStream = FileOutputStream(File(getPath(key)))
+ kryoOutput = Output(fileStream)
+ getKryo().writeObject(kryoOutput, kiteTable)
+ kryoOutput.flush()
+ fileStream.flush()
+ kryoOutput.close()
+ } catch (e: Exception) {
+ throw KiteDbException(e.message)
+ }
+ }
+
+ fun read(key: String): Any? {
+ val keyFile = File(getPath(key))
+ if (!keyFile.exists()) return null
+ val kryoInput = Input(FileInputStream(keyFile))
+ val kiteTable = getKryo().readObject(kryoInput, KiteTable::class.java)
+ return kiteTable.mContent
+ }
+
+ fun delete(key: String): Boolean {
+ val keyFile = File(getPath(key))
+ return if (keyFile.exists()) {
+ keyFile.delete()
+ } else {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/local/KiteDbException.kt b/app/src/main/java/com/aab2apk/local/KiteDbException.kt
new file mode 100644
index 0000000..060882c
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/local/KiteDbException.kt
@@ -0,0 +1,9 @@
+package com.aab2apk.original
+
+
+class KiteDbException : RuntimeException {
+
+ constructor(detailMessage: String?) : super(detailMessage)
+
+ constructor(detailMessage: String?, throwable: Throwable?) : super(detailMessage, throwable)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/local/KiteTable.kt b/app/src/main/java/com/aab2apk/local/KiteTable.kt
new file mode 100644
index 0000000..2703be5
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/local/KiteTable.kt
@@ -0,0 +1,12 @@
+package com.aab2apk.original
+
+
+class KiteTable {
+ internal constructor()
+ internal constructor(content: T) {
+ mContent = content
+ }
+
+ // Serialized content
+ var mContent: T? = null
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/ui/Styles.kt b/app/src/main/java/com/aab2apk/ui/Styles.kt
new file mode 100644
index 0000000..98ad5cf
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/Styles.kt
@@ -0,0 +1,37 @@
+package com.aab2apk.original
+
+
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.TextUnit
+
+object Styles {
+
+ fun TextStyleNormal(size: TextUnit) =
+ TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontFamily = codeFontFamily,
+ fontSize = size
+ )
+
+ fun TextStyleMedium(size: TextUnit) =
+ TextStyle(
+ fontWeight = FontWeight.Medium,
+ fontFamily = codeFontFamily,
+ fontSize = size
+ )
+
+ fun TextStyleSemiBold(size: TextUnit) =
+ TextStyle(
+ fontWeight = FontWeight.SemiBold,
+ fontFamily = codeFontFamily,
+ fontSize = size
+ )
+
+ fun TextStyleBold(size: TextUnit) =
+ TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontFamily = codeFontFamily,
+ fontSize = size
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/ui/Typography.kt b/app/src/main/java/com/aab2apk/ui/Typography.kt
new file mode 100644
index 0000000..0ab8568
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/Typography.kt
@@ -0,0 +1,85 @@
+package com.aab2apk.original
+
+
+import androidx.compose.material.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.platform.Font
+import androidx.compose.ui.unit.sp
+
+val codeFontFamily = FontFamily(
+ Font(resource = "fonts/sans_regular.ttf", weight = FontWeight.Light),
+ Font(resource = "fonts/sans_thin.ttf", weight = FontWeight.Normal),
+ Font(resource = "fonts/sans_medium.ttf", weight = FontWeight.Medium),
+ Font(resource = "fonts/sans_bold.ttf", weight = FontWeight.Bold)
+)
+
+val typography = Typography(
+ defaultFontFamily = codeFontFamily,
+ h1 = TextStyle(
+ fontWeight = FontWeight.Light,
+ fontSize = 96.sp,
+ letterSpacing = (-1.5).sp
+ ),
+ h2 = TextStyle(
+ fontWeight = FontWeight.Light,
+ fontSize = 60.sp,
+ letterSpacing = (-0.5).sp
+ ),
+ h3 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 48.sp,
+ letterSpacing = 0.sp
+ ),
+ h4 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 30.sp,
+ letterSpacing = 0.sp
+ ),
+ h5 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 24.sp,
+ letterSpacing = 0.sp
+ ),
+ h6 = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 20.sp,
+ letterSpacing = 0.sp
+ ),
+ subtitle1 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ letterSpacing = 0.15.sp
+ ),
+ subtitle2 = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ letterSpacing = 0.1.sp
+ ),
+ body1 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 16.sp,
+ letterSpacing = 0.5.sp
+ ),
+ body2 = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 14.sp,
+ letterSpacing = 0.25.sp
+ ),
+ button = TextStyle(
+ fontWeight = FontWeight.Bold,
+ fontSize = 14.sp,
+ letterSpacing = 0.25.sp
+ ),
+ caption = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 12.sp,
+ letterSpacing = 0.4.sp
+ ),
+ overline = TextStyle(
+ fontWeight = FontWeight.Normal,
+ fontSize = 10.sp,
+ letterSpacing = 1.sp
+ )
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/ui/components/ButtonWithToolTip.kt b/app/src/main/java/com/aab2apk/ui/components/ButtonWithToolTip.kt
new file mode 100644
index 0000000..48497ce
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/components/ButtonWithToolTip.kt
@@ -0,0 +1,85 @@
+package com.aab2apk.original
+
+
+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.ButtonDefaults
+import androidx.compose.material.Icon
+import androidx.compose.material.Surface
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonColors
+import androidx.compose.material.Text
+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/app/src/main/java/com/aab2apk/ui/components/CheckboxWithText.kt b/app/src/main/java/com/aab2apk/ui/components/CheckboxWithText.kt
new file mode 100644
index 0000000..f2dcb08
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/components/CheckboxWithText.kt
@@ -0,0 +1,78 @@
+package com.aab2apk.original
+
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.TooltipArea
+import androidx.compose.foundation.TooltipPlacement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Checkbox
+import androidx.compose.material.Icon
+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.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.res.loadSvgPainter
+import androidx.compose.ui.res.useResource
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import ui.Styles
+import utils.TestTags
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun CheckboxWithText(label: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, toolTipText: String = "") {
+ val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML)
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier.padding(end = 16.dp)
+ ) {
+ Checkbox(
+ modifier = Modifier.testTag(TestTags.CHECKBOX_TAG),
+ checked = isChecked,
+ onCheckedChange = { onCheckedChange.invoke(it) }
+ )
+ Text(
+ modifier = Modifier.testTag(TestTags.TEXT_TAG),
+ text = label,
+ style = Styles.TextStyleMedium(14.sp)
+ )
+ 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,
+
+ 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("info.svg") { loadSvgPainter(it, density) },
+ contentDescription = "Info",
+ modifier = Modifier.padding(start = 8.dp)
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/ui/components/ChooseFileTextField.kt b/app/src/main/java/com/aab2apk/ui/components/ChooseFileTextField.kt
new file mode 100644
index 0000000..f958114
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/components/ChooseFileTextField.kt
@@ -0,0 +1,75 @@
+package com.aab2apk.original
+
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.wrapContentWidth
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Button
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.TextField
+import androidx.compose.material.TextFieldDefaults
+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 ui.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.4f).height(50.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(50.dp)
+ .wrapContentWidth()
+ ) {
+ Icon(
+ painter = useResource("open_folder.svg") { loadSvgPainter(it, density) },
+ contentDescription = ""
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/ui/components/CustomTextField.kt b/app/src/main/java/com/aab2apk/ui/components/CustomTextField.kt
new file mode 100644
index 0000000..7f08a10
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/components/CustomTextField.kt
@@ -0,0 +1,62 @@
+package com.aab2apk.original
+
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.material.TextFieldDefaults
+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.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import ui.Styles
+
+@Composable
+fun CustomTextField(value: String, label: String, forPassword: Boolean = true, onValueChange: (String) -> Unit) {
+ Row(
+ modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ TextField(
+ modifier = Modifier.fillMaxWidth(0.4f).height(50.dp)
+ .border(
+ BorderStroke(width = 2.dp, color = MaterialTheme.colors.primary),
+ shape = RoundedCornerShape(topEnd = 10.dp, bottomEnd = 10.dp, topStart = 10.dp, bottomStart = 10.dp)
+ ),
+ value = value,
+ singleLine = true,
+ visualTransformation = if (forPassword) PasswordVisualTransformation() else VisualTransformation.None,
+ keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+ 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 = {
+ onValueChange.invoke(it)
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/ui/components/LoadingDialog.kt b/app/src/main/java/com/aab2apk/ui/components/LoadingDialog.kt
new file mode 100644
index 0000000..7a8eab5
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/components/LoadingDialog.kt
@@ -0,0 +1,61 @@
+package com.aab2apk.original
+
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+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/app/src/main/java/com/aab2apk/ui/components/TextWithIcon.kt b/app/src/main/java/com/aab2apk/ui/components/TextWithIcon.kt
new file mode 100644
index 0000000..495a9a0
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/ui/components/TextWithIcon.kt
@@ -0,0 +1,37 @@
+package com.aab2apk.original
+
+
+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/app/src/main/java/com/aab2apk/utils/Constant.kt b/app/src/main/java/com/aab2apk/utils/Constant.kt
new file mode 100644
index 0000000..6c89efe
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/Constant.kt
@@ -0,0 +1,26 @@
+package com.aab2apk.original
+
+
+object Constant {
+ const val SUCCESS = 0
+ const val FAILURE = 1
+ const val BUNDLE_DOWNLOAD_LINK = "https://github.com/google/bundletool/releases"
+}
+
+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 {
+ const val DEBUG = 1
+ const val RELEASE = 2
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/utils/FileHelper.kt b/app/src/main/java/com/aab2apk/utils/FileHelper.kt
new file mode 100644
index 0000000..c931584
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/FileHelper.kt
@@ -0,0 +1,32 @@
+package com.aab2apk.original
+
+
+import java.io.File
+import java.io.IOException
+
+object FileHelper {
+
+ fun performFileOperations(directory: String, fileName: String, fileStatus: (Int, String) -> Unit) {
+ val oldFile = File(directory, "${fileName.split(".")[0]}.apks")
+ val newFile = File(directory, "${fileName.split(".")[0]}.zip")
+ if (FileUtils.renameFile(oldFile, newFile)) {
+ Log.i("CHANGED NAME\n Unzipping...")
+ try {
+ FileUtils.unzip(newFile, directory)
+ fileStatus.invoke(
+ Constant.SUCCESS,
+ "Rename and Unzip Successful\nFile Saved at ${directory.removeSuffix("\\")}."
+ )
+ Log.i("TRYING DELETING FILE")
+ val value = FileUtils.deleteFile(newFile)
+ Log.i("DELETE STATUS : $value")
+ } catch (exception: IOException) {
+ Log.i("Unzipping Failed: ${exception.printStackTrace()}")
+ fileStatus.invoke(Constant.FAILURE, "Unzipping Failed: ${exception.printStackTrace()}")
+ }
+ } else {
+ Log.i("FAILED RENAMING THE FILE!!")
+ fileStatus.invoke(Constant.FAILURE, "FAILED RENAMING THE FILE!!")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/utils/FileUtils.kt b/app/src/main/java/com/aab2apk/utils/FileUtils.kt
new file mode 100644
index 0000000..c55f1b0
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/FileUtils.kt
@@ -0,0 +1,81 @@
+package com.aab2apk.original
+
+
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.IOException
+import java.io.InputStream
+import java.io.FileOutputStream
+import java.nio.file.Paths
+import java.util.zip.ZipFile
+
+object FileUtils {
+
+ /**
+ * Size of the buffer to read/write data
+ */
+ private const val BUFFER_SIZE = 4096
+
+ fun deleteFile(file: File) =
+ file.delete()
+
+ /**
+ * @param oldFile
+ * @param newFile
+ */
+ fun renameFile(oldFile: File, newFile: File) =
+ oldFile.renameTo(newFile)
+
+ /**
+ * @param zipFilePath
+ * @param destDirectory
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ fun unzip(zipFilePath: File, destDirectory: String, skipFile: String = "toc.pb") {
+
+ File(destDirectory).run {
+ if (!exists()) {
+ mkdirs()
+ }
+ }
+
+ ZipFile(zipFilePath).use { zip ->
+
+ zip.entries().asSequence().forEach { entry ->
+ if (entry.name == skipFile) return@forEach
+
+ zip.getInputStream(entry).use { input ->
+ val safeEntryName = Paths.get(entry.name).normalize().toString()
+ val filePath = destDirectory + File.separator + safeEntryName
+
+ if (!entry.isDirectory) {
+ // if the entry is a file, extracts it
+ extractFile(input, filePath)
+ } else {
+ // if the entry is a directory, make the directory
+ val dir = File(filePath)
+ dir.mkdir()
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Extracts a zip entry (file entry)
+ * @param inputStream
+ * @param destFilePath
+ * @throws IOException
+ */
+ @Throws(IOException::class)
+ private fun extractFile(inputStream: InputStream, destFilePath: String) {
+ val bos = BufferedOutputStream(FileOutputStream(destFilePath))
+ val bytesIn = ByteArray(BUFFER_SIZE)
+ var read: Int
+ while (inputStream.read(bytesIn).also { read = it } != -1) {
+ bos.write(bytesIn, 0, read)
+ }
+ bos.close()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/utils/Log.kt b/app/src/main/java/com/aab2apk/utils/Log.kt
new file mode 100644
index 0000000..e39a2e5
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/Log.kt
@@ -0,0 +1,12 @@
+package com.aab2apk.original
+
+
+object Log {
+
+ var showLogs: Boolean = true
+ fun i(message: String) {
+ if (showLogs) {
+ println(message)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/utils/Strings.kt b/app/src/main/java/com/aab2apk/utils/Strings.kt
new file mode 100644
index 0000000..c3b9352
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/Strings.kt
@@ -0,0 +1,45 @@
+package com.aab2apk.original
+
+
+object Strings {
+ const val APP_NAME = "Android Bundletool UI"
+ const val URL = "URL"
+ const val CHOOSE_FILE = "Choose a file"
+ const val SELECT_BUNDLETOOL_JAR = "Select Bundletool Jar"
+ const val SAVE_JAR_PATH = "Save Jar Path"
+ const val SAVE_JAR_PATH_INFO = "Saves Path of Bundle Tool Jar for Future."
+ const val DOWNLOAD_BUNDLETOOL = "Download Bundletool from here"
+ const val SELECT_AAB_FILE = "Select Aab File"
+ const val OPTIONS_FOR_BUILD_APKS = "Options for the bundletool build-apks command"
+ const val OVERWRITE = "Overwrite"
+ const val OVERWRITE_INFO = "Overwrites any existing output file with the path you specify using the --output option. If you don't include this flag and the output file already exists, you get a build error."
+ const val AAPT2_PATH = "Aapt2 Path"
+ const val AAPT2_PATH_INFO = "Specifies a custom path to AAPT2. By default, bundletool includes its own version of AAPT2."
+ const val SELECT_AAPT2_FILE = "Select Aapt2 File"
+ const val MODE_UNIVERSAL = "Mode Universal"
+ const val MODE_UNIVERSAL_INFO = "Sets the mode to universal. Use this option if you want bundletool to build a single APK that includes all of your app's code and resources, so that the APK is compatible with all device configurations your app supports." +
+ "\nNote: bundletool includes only feature modules that specify in their manifest in a universal APK. To learn more, read about the feature module manifest.\n" +
+ "Keep in mind, these APKs are larger than those optimized for a particular device configuration. However, they're easier to share with internal testers who, for example, want to test your app on multiple device configurations."
+ const val SIGNING_MODE = "Signing Mode"
+ const val DEBUG = "Debug"
+ const val RELEASE = "Release"
+ const val KEYSTORE_PATH = "Select Keystore Path"
+ const val KEYSTORE_PASSWORD = "Keystore Password"
+ const val KEY_ALIAS = "Key Alias"
+ 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 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. Use with Mode Universal to get one apk."
+ const val VERIFYING_ADB_PATH = "Verifying ADB Path.."
+ const val SERIAL_ID = "Serial Id"
+ const val FETCH_DEVICES = "Fetch Devices"
+ const val FETCH_DEVICES_INFO = "Fetch Connected Devices Info in LogView"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/utils/TestTags.kt b/app/src/main/java/com/aab2apk/utils/TestTags.kt
new file mode 100644
index 0000000..a483d69
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/TestTags.kt
@@ -0,0 +1,7 @@
+package com.aab2apk.original
+
+
+object TestTags {
+ const val CHECKBOX_TAG = "CHECKBOX_TAG"
+ const val TEXT_TAG = "TEXT_TAG"
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/aab2apk/utils/Utils.kt b/app/src/main/java/com/aab2apk/utils/Utils.kt
new file mode 100644
index 0000000..77e2e0c
--- /dev/null
+++ b/app/src/main/java/com/aab2apk/utils/Utils.kt
@@ -0,0 +1,11 @@
+package com.aab2apk.original
+
+
+import java.util.Locale
+
+object Utils {
+
+ fun isWindowsOS(): Boolean {
+ return System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows")
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..c736a21
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..a6601e0
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id 'com.android.application' version '8.13.1' apply false
+ id 'org.jetbrains.kotlin.jvm' version '1.8.0' apply false
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 0fcfeec..f3b54e7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,58 +1,4 @@
-import org.jetbrains.compose.desktop.application.dsl.TargetFormat
-
plugins {
- kotlin("multiplatform")
- id("org.jetbrains.compose")
-}
-
-group = "com.AabToApk"
-version = "1.0-SNAPSHOT"
-
-repositories {
- google()
- mavenCentral()
- maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
-}
-
-kotlin {
- jvm {
- jvmToolchain(11)
- withJava()
- }
- sourceSets {
- val jvmMain by getting {
- dependencies {
- implementation(compose.desktop.currentOs)
- api("com.esotericsoftware:kryo:4.0.1")
- }
- }
- val jvmTest by getting {
- dependencies {
- implementation(compose.desktop.uiTestJUnit4)
- implementation(compose.desktop.currentOs)
- }
- }
- val resourcesDir = "src/common/resources"
- }
-}
-
-compose.desktop {
- application {
- mainClass = "MainKt"
- nativeDistributions {
- targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
- packageName = "AabToApk"
- packageVersion = "1.0.4"
- val iconsRoot = project.file("desktop-icons")
- macOS{
- iconFile.set(iconsRoot.resolve("launcher.icns"))
- }
- windows{
- iconFile.set(iconsRoot.resolve("launcher.ico"))
- }
- linux{
- iconFile.set(iconsRoot.resolve("launcher.png"))
- }
- }
- }
+ id("com.android.application") version "8.13.1" apply false
+ kotlin("jvm") version (extra["kotlin.version"] as String) apply false
}
diff --git a/gradle.properties b/gradle.properties
index 362b3b4..51ee65e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,5 @@
kotlin.code.style=official
kotlin.version=1.8.0
-agp.version=7.3.0
-compose.version=1.6.0
\ No newline at end of file
+agp.version=8.13.1
+compose.version=1.6.0
+ndk.version=28.2.13676358
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index ae04661..d4081da 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8b8f1b2..7777163 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,16 +1,9 @@
pluginManagement {
repositories {
- google()
gradlePluginPortal()
+ google()
mavenCentral()
- maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
- }
-
- plugins {
- kotlin("multiplatform").version(extra["kotlin.version"] as String)
- id("org.jetbrains.compose").version(extra["compose.version"] as String)
}
}
-
-rootProject.name = "AabToApk"
-
+rootProject.name = "aab2apk"
+include(":app")