#!/bin/sh set -eu SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd) REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) SOURCE_ICON="$REPO_ROOT/src-tauri/icons/icon.png" ANDROID_APP_DIR="$REPO_ROOT/src-tauri/gen/android/app" ANDROID_RES_DIR="$ANDROID_APP_DIR/src/main/res" ANDROID_VALUES_DIR="$ANDROID_RES_DIR/values" ANDROID_VALUES_NIGHT_DIR="$ANDROID_RES_DIR/values-night" ANDROID_GRADLE_FILE="$ANDROID_APP_DIR/build.gradle.kts" ANDROID_MAIN_ACTIVITY="$ANDROID_APP_DIR/src/main/java/in/cinny/app/MainActivity.kt" ANDROID_BUILDTASK_FILE="$REPO_ROOT/src-tauri/gen/android/buildSrc/src/main/java/in/cinny/app/kotlin/BuildTask.kt" if [ ! -f "$SOURCE_ICON" ]; then echo "Missing source icon: $SOURCE_ICON" >&2 exit 1 fi if [ ! -d "$ANDROID_RES_DIR" ]; then echo "Missing Android resources directory: $ANDROID_RES_DIR" >&2 echo "Run android init first." >&2 exit 1 fi mkdir -p "$ANDROID_VALUES_DIR" "$ANDROID_VALUES_NIGHT_DIR" resize_icon() { size="$1" output="$2" sips -z "$size" "$size" "$SOURCE_ICON" --out "$output" >/dev/null } for spec in \ "48 mipmap-mdpi" \ "72 mipmap-hdpi" \ "96 mipmap-xhdpi" \ "144 mipmap-xxhdpi" \ "192 mipmap-xxxhdpi" do set -- $spec resize_icon "$1" "$ANDROID_RES_DIR/$2/ic_launcher.png" resize_icon "$1" "$ANDROID_RES_DIR/$2/ic_launcher_round.png" done for spec in \ "108 mipmap-mdpi" \ "162 mipmap-hdpi" \ "216 mipmap-xhdpi" \ "324 mipmap-xxhdpi" \ "432 mipmap-xxxhdpi" do set -- $spec resize_icon "$1" "$ANDROID_RES_DIR/$2/ic_launcher_foreground.png" done cat > "$ANDROID_GRADLE_FILE" <<'EOF' import java.io.FileInputStream import java.util.Properties plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("rust") } val tauriProperties = Properties().apply { val propFile = file("tauri.properties") if (propFile.exists()) { propFile.inputStream().use { load(it) } } } android { compileSdk = 36 namespace = "in.cinny.app" defaultConfig { manifestPlaceholders["usesCleartextTraffic"] = "true" applicationId = "in.cinny.app" minSdk = 24 targetSdk = 36 versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") } signingConfigs { create("release") { val keystorePropertiesFile = rootProject.file("keystore.properties") val keystoreProperties = Properties() if (keystorePropertiesFile.exists()) { keystoreProperties.load(FileInputStream(keystorePropertiesFile)) } keyAlias = keystoreProperties["keyAlias"] as String keyPassword = keystoreProperties["password"] as String storeFile = file(keystoreProperties["storeFile"] as String) storePassword = keystoreProperties["password"] as String } } buildTypes { getByName("debug") { manifestPlaceholders["usesCleartextTraffic"] = "true" isDebuggable = true isJniDebuggable = true isMinifyEnabled = false packaging { jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") jniLibs.keepDebugSymbols.add("*/x86/*.so") jniLibs.keepDebugSymbols.add("*/x86_64/*.so") } } getByName("release") { signingConfig = signingConfigs.getByName("release") isMinifyEnabled = true proguardFiles( *fileTree(".") { include("**/*.pro") } .plus(getDefaultProguardFile("proguard-android-optimize.txt")) .toList().toTypedArray() ) } } kotlinOptions { jvmTarget = "1.8" } buildFeatures { buildConfig = true } } rust { rootDirRel = "../../../" } dependencies { implementation("androidx.webkit:webkit:1.14.0") implementation("androidx.appcompat:appcompat:1.7.1") implementation("androidx.activity:activity-ktx:1.10.1") implementation("com.google.android.material:material:1.12.0") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.4") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") } apply(from = "tauri.build.gradle.kts") EOF cat > "$ANDROID_MAIN_ACTIVITY" <<'EOF' package `in`.cinny.app import android.os.Bundle import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding class MainActivity : TauriActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val content = findViewById(android.R.id.content) ViewCompat.setOnApplyWindowInsetsListener(content) { view, windowInsets -> val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) view.updatePadding( left = insets.left, top = insets.top, right = insets.right, bottom = insets.bottom, ) windowInsets } } } EOF cat > "$ANDROID_BUILDTASK_FILE" <<'EOF' import java.io.File import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.logging.LogLevel import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction open class BuildTask : DefaultTask() { @Input var rootDirRel: String? = null @Input var target: String? = null @Input var release: Boolean? = null @TaskAction fun assemble() { val executable = """node""" try { runTauriCli(executable) } catch (e: Exception) { if (Os.isFamily(Os.FAMILY_WINDOWS)) { runTauriCli("$executable.cmd") } else { throw e } } } fun runTauriCli(executable: String) { val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") val target = target ?: throw GradleException("target cannot be null") val release = release ?: throw GradleException("release cannot be null") val tauriCli = File( project.projectDir, "${rootDirRel}/../node_modules/@tauri-apps/cli/tauri.js" ).canonicalPath val args = listOf(tauriCli, "android", "android-studio-script") project.exec { workingDir(File(project.projectDir, rootDirRel)) executable(executable) args(args) if (project.logger.isEnabled(LogLevel.DEBUG)) { args("-vv") } else if (project.logger.isEnabled(LogLevel.INFO)) { args("-v") } if (release) { args("--release") } args(listOf("--target", target)) }.assertNormalExitValue() } } EOF cat > "$ANDROID_VALUES_DIR/colors.xml" <<'EOF' #1A1A1A EOF cat > "$ANDROID_VALUES_NIGHT_DIR/colors.xml" <<'EOF' #1A1A1A EOF cat > "$ANDROID_VALUES_DIR/themes.xml" <<'EOF' EOF cat > "$ANDROID_VALUES_NIGHT_DIR/themes.xml" <<'EOF' EOF