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")