mirror of
https://github.com/izzy2lost/aab2apk.git
synced 2026-06-19 01:20:08 -07:00
groundwork
This commit is contained in:
@@ -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
|
||||
+15
-38
@@ -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
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
### Markdown files ###
|
||||
*.md
|
||||
!README.md
|
||||
@@ -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")
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
# proguard rules
|
||||
@@ -0,0 +1,10 @@
|
||||
<manifest package="com.aab2apk" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application android:label="aab2apk" android:allowBackup="true" android:icon="@mipmap/ic_launcher">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
# Placeholder bundletool binary
|
||||
# REPLACE THIS FILE with a native ARM64 bundletool executable built with GraalVM.
|
||||
@@ -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<String> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> = 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<String, String>) = 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<String, Boolean> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <E> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.aab2apk.original
|
||||
|
||||
|
||||
class KiteDbException : RuntimeException {
|
||||
|
||||
constructor(detailMessage: String?) : super(detailMessage)
|
||||
|
||||
constructor(detailMessage: String?, throwable: Throwable?) : super(detailMessage, throwable)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.aab2apk.original
|
||||
|
||||
|
||||
class KiteTable<T> {
|
||||
internal constructor()
|
||||
internal constructor(content: T) {
|
||||
mContent = content
|
||||
}
|
||||
|
||||
// Serialized content
|
||||
var mContent: T? = null
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user