diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts new file mode 100644 index 000000000..1e658cae4 --- /dev/null +++ b/androidApp/build.gradle.kts @@ -0,0 +1,118 @@ +import java.util.Properties + +fun readXcconfigValue(file: File, key: String): String? { + if (!file.exists()) return null + return file.readLines() + .asSequence() + .map(String::trim) + .filter { it.isNotEmpty() && !it.startsWith("#") && it.contains('=') } + .map { line -> + val separatorIndex = line.indexOf('=') + line.substring(0, separatorIndex).trim() to line.substring(separatorIndex + 1).trim() + } + .firstOrNull { (entryKey, _) -> entryKey == key } + ?.second +} + +plugins { + alias(libs.plugins.androidApplication) +} + +val localProps = Properties().apply { + val propsFile = rootProject.file("local.properties") + if (propsFile.exists()) propsFile.inputStream().use { load(it) } +} +val releaseStoreFile = localProps.getProperty("NUVIO_RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() } +val releaseStorePassword = localProps.getProperty("NUVIO_RELEASE_STORE_PASSWORD")?.takeIf { it.isNotBlank() } +val releaseKeyAlias = localProps.getProperty("NUVIO_RELEASE_KEY_ALIAS")?.takeIf { it.isNotBlank() } +val releaseKeyPassword = localProps.getProperty("NUVIO_RELEASE_KEY_PASSWORD")?.takeIf { it.isNotBlank() } +val releaseKeystore = releaseStoreFile?.let(rootProject::file) +val appVersionConfigFile = rootProject.file("iosApp/Configuration/Version.xcconfig") +val releaseAppVersionName = readXcconfigValue(appVersionConfigFile, "MARKETING_VERSION") + ?: error("MARKETING_VERSION is missing from ${appVersionConfigFile.path}") +val releaseAppVersionCode = readXcconfigValue(appVersionConfigFile, "CURRENT_PROJECT_VERSION") + ?.toIntOrNull() + ?: error("CURRENT_PROJECT_VERSION is missing or invalid in ${appVersionConfigFile.path}") + +android { + namespace = "com.nuvio.android" + compileSdk = libs.versions.android.compileSdk.get().toInt() + compileSdkMinor = libs.versions.android.compileSdkMinor.get().toInt() + + signingConfigs { + create("release") { + if (releaseKeystore != null && releaseStorePassword != null && releaseKeyAlias != null && releaseKeyPassword != null) { + storeFile = releaseKeystore + storePassword = releaseStorePassword + keyAlias = releaseKeyAlias + keyPassword = releaseKeyPassword + } + } + } + + defaultConfig { + applicationId = "com.nuvio.app" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = releaseAppVersionCode + versionName = releaseAppVersionName + } + + flavorDimensions += "distribution" + productFlavors { + create("full") { + dimension = "distribution" + } + create("playstore") { + dimension = "distribution" + } + } + + sourceSets.getByName("full") { + manifest.srcFile("src/full/AndroidManifest.xml") + jniLibs.directories.add("../composeApp/src/full/jniLibs") + } + + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + jniLibs { + useLegacyPackaging = true + pickFirsts += listOf( + "lib/*/libc++_shared.so", + "lib/*/libavcodec.so", + "lib/*/libavutil.so", + "lib/*/libswscale.so", + "lib/*/libswresample.so" + ) + } + } + + buildTypes { + getByName("release") { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "../composeApp/proguard-rules.pro", + ) + signingConfig = signingConfigs.getByName("release") + ndk { + debugSymbolLevel = "FULL" + } + } + } + + compileOptions { + isCoreLibraryDesugaringEnabled = true + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(project(":composeApp")) + coreLibraryDesugaring(libs.desugar.jdk.libs) + debugImplementation(libs.compose.uiTooling) +} diff --git a/androidApp/src/full/AndroidManifest.xml b/androidApp/src/full/AndroidManifest.xml new file mode 100644 index 000000000..47497cd7b --- /dev/null +++ b/androidApp/src/full/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml new file mode 100644 index 000000000..0d702e768 --- /dev/null +++ b/androidApp/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle.kts b/build.gradle.kts index cf780d5a6..11682b292 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,8 @@ plugins { // in each subproject's classloader alias(libs.plugins.androidApplication) apply false alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.androidKotlinMultiplatformLibrary) apply false alias(libs.plugins.composeMultiplatform) apply false alias(libs.plugins.composeCompiler) apply false alias(libs.plugins.kotlinMultiplatform) apply false -} \ No newline at end of file +} diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 964b76961..7ceba3d37 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.compose.desktop.application.dsl.TargetFormat import org.gradle.api.DefaultTask import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty @@ -176,7 +175,7 @@ fun readXcconfigValue(file: File, key: String): String? { plugins { alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidApplication) + alias(libs.plugins.androidKotlinMultiplatformLibrary) alias(libs.plugins.composeMultiplatform) alias(libs.plugins.composeCompiler) alias(libs.plugins.kotlinxSerialization) @@ -186,11 +185,6 @@ val supabaseProps = Properties().apply { val propsFile = rootProject.file("local.properties") if (propsFile.exists()) propsFile.inputStream().use { load(it) } } -val releaseStoreFile = supabaseProps.getProperty("NUVIO_RELEASE_STORE_FILE")?.takeIf { it.isNotBlank() } -val releaseStorePassword = supabaseProps.getProperty("NUVIO_RELEASE_STORE_PASSWORD")?.takeIf { it.isNotBlank() } -val releaseKeyAlias = supabaseProps.getProperty("NUVIO_RELEASE_KEY_ALIAS")?.takeIf { it.isNotBlank() } -val releaseKeyPassword = supabaseProps.getProperty("NUVIO_RELEASE_KEY_PASSWORD")?.takeIf { it.isNotBlank() } -val releaseKeystore = releaseStoreFile?.let(rootProject::file) val appVersionConfigFile = rootProject.file("iosApp/Configuration/Version.xcconfig") val releaseAppVersionName = readXcconfigValue(appVersionConfigFile, "MARKETING_VERSION") ?: error("MARKETING_VERSION is missing from ${appVersionConfigFile.path}") @@ -217,13 +211,38 @@ val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-con val requestedGradleTasks = gradle.startParameter.taskNames.map { taskName -> taskName.substringAfterLast(':').lowercase() } -val isAndroidAppBundleBuild = requestedGradleTasks.any { taskName -> - taskName == "bundle" || - taskName == "bundlerelease" || - taskName == "bundledebug" || - taskName.startsWith("bundleplaystore") || - taskName.startsWith("bundlefull") || - taskName.endsWith("bundle") +val requestedAndroidDistributions = requestedGradleTasks.mapNotNull { taskName -> + when { + "playstore" in taskName -> "playstore" + "full" in taskName -> "full" + else -> null + } +}.toSet() +require(requestedAndroidDistributions.size <= 1) { + "Build Android full and playstore distributions separately, or set -Pnuvio.android.distribution=full|playstore." +} +val configuredAndroidDistribution = providers.gradleProperty("nuvio.android.distribution").orNull + ?: supabaseProps.getProperty("NUVIO_ANDROID_DISTRIBUTION") +val isAmbiguousAndroidPackageTask = requestedGradleTasks.any { taskName -> + taskName == "build" || + taskName.startsWith("assemble") || + taskName.startsWith("bundle") +} && requestedAndroidDistributions.isEmpty() +require(configuredAndroidDistribution != null || !isAmbiguousAndroidPackageTask) { + "Set -Pnuvio.android.distribution=full|playstore for aggregate Android assemble/bundle tasks." +} +val androidDistribution = ( + configuredAndroidDistribution + ?: requestedAndroidDistributions.singleOrNull() + ?: "playstore" + ).trim().lowercase() +require(androidDistribution == "playstore" || androidDistribution == "full") { + "nuvio.android.distribution must be 'playstore' or 'full'." +} +val androidDistributionSourceDir = if (androidDistribution == "full") { + "src/androidFull/kotlin" +} else { + "src/androidPlaystore/kotlin" } val runtimeLocalProperties = Properties().apply { val file = rootProject.file("local.properties") @@ -254,7 +273,16 @@ tasks.withType>().configureEach { } kotlin { - androidTarget { + android { + namespace = "com.nuvio.app" + compileSdk { + version = release(libs.versions.android.compileSdk.get().toInt()) { + minorApiLevel = libs.versions.android.compileSdkMinor.get().toInt() + } + } + minSdk = libs.versions.android.minSdk.get().toInt() + androidResources.enable = true + compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } @@ -298,30 +326,41 @@ kotlin { val commonMain by getting { kotlin.srcDir(generatedRuntimeConfigDir) } - androidMain.dependencies { - implementation(libs.compose.uiToolingPreview) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.core.splashscreen) - implementation(libs.androidx.work.runtime) - implementation(libs.coil.gif) - implementation("androidx.recyclerview:recyclerview:1.4.0") - implementation("com.squareup.okhttp3:okhttp:4.12.0") - implementation("com.google.code.gson:gson:2.11.0") - implementation("io.github.peerless2012:ass-media:0.4.0-beta01") - implementation(libs.ktor.client.android) - implementation(libs.androidx.media3.exoplayer.hls) - implementation(libs.androidx.media3.exoplayer.dash) - implementation(libs.androidx.media3.exoplayer.smoothstreaming) - implementation(libs.androidx.media3.exoplayer.rtsp) - implementation(libs.androidx.media3.datasource) - implementation(libs.androidx.media3.datasource.okhttp) - implementation(libs.androidx.media3.decoder) - implementation(libs.androidx.media3.session) - implementation(libs.androidx.media3.common) - implementation(libs.androidx.media3.container) - implementation(libs.androidx.media3.extractor) - implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar")))) + androidMain { + kotlin.srcDir(project.file(androidDistributionSourceDir)) + if (androidDistribution == "full") { + kotlin.srcDir(fullCommonSourceDir) + } + + dependencies { + implementation(libs.compose.uiToolingPreview) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.core.splashscreen) + implementation(libs.androidx.work.runtime) + implementation(libs.coil.gif) + implementation("androidx.recyclerview:recyclerview:1.4.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.11.0") + implementation("io.github.peerless2012:ass-media:0.4.0-beta01") + implementation(libs.ktor.client.android) + implementation(libs.androidx.media3.exoplayer.hls) + implementation(libs.androidx.media3.exoplayer.dash) + implementation(libs.androidx.media3.exoplayer.smoothstreaming) + implementation(libs.androidx.media3.exoplayer.rtsp) + implementation(libs.androidx.media3.datasource) + implementation(libs.androidx.media3.datasource.okhttp) + implementation(libs.androidx.media3.decoder) + implementation(libs.androidx.media3.session) + implementation(libs.androidx.media3.common) + implementation(libs.androidx.media3.container) + implementation(libs.androidx.media3.extractor) + implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("lib-*.aar")))) + if (androidDistribution == "full") { + implementation(files("libs/quickjs-kt-android-1.0.5-nuvio.aar")) + implementation(libs.ksoup) + } + } } commonMain.dependencies { implementation(libs.coil.compose) @@ -351,94 +390,11 @@ kotlin { } } -afterEvaluate { - dependencies { - add("fullImplementation", files("libs/quickjs-kt-android-1.0.5-nuvio.aar")) - add("fullImplementation", libs.ksoup) - } -} - configurations.matching { it.name == "iosMainImplementation" }.configureEach { project.dependencies.add(name, libs.ktor.client.darwin) } -dependencies { - coreLibraryDesugaring(libs.desugar.jdk.libs) - debugImplementation(libs.compose.uiTooling) -} - configurations.all { exclude(group = "androidx.media3", module = "media3-exoplayer") exclude(group = "androidx.media3", module = "media3-ui") } - -android { - namespace = "com.nuvio.app" - compileSdk = libs.versions.android.compileSdk.get().toInt() - - signingConfigs { - create("release") { - if (releaseKeystore != null && releaseStorePassword != null && releaseKeyAlias != null && releaseKeyPassword != null) { - storeFile = releaseKeystore - storePassword = releaseStorePassword - keyAlias = releaseKeyAlias - keyPassword = releaseKeyPassword - } - } - } - - defaultConfig { - applicationId = "com.nuvio.app" - minSdk = libs.versions.android.minSdk.get().toInt() - targetSdk = libs.versions.android.targetSdk.get().toInt() - versionCode = releaseAppVersionCode - versionName = releaseAppVersionName - } - flavorDimensions += "distribution" - productFlavors { - create("full") { - dimension = "distribution" - } - create("playstore") { - dimension = "distribution" - } - } - sourceSets.getByName("full") { - manifest.srcFile("src/androidFull/AndroidManifest.xml") - java.srcDir(fullCommonSourceDir) - } - packaging { - resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" - } - jniLibs { - useLegacyPackaging = true - pickFirsts += listOf( - "lib/*/libc++_shared.so", - "lib/*/libavcodec.so", - "lib/*/libavutil.so", - "lib/*/libswscale.so", - "lib/*/libswresample.so" - ) - } - } - buildTypes { - getByName("release") { - isMinifyEnabled = true - isShrinkResources = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - signingConfig = signingConfigs.getByName("release") - ndk { - debugSymbolLevel = "FULL" - } - } - } - compileOptions { - isCoreLibraryDesugaringEnabled = true - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } -} diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml index 855965355..8072ee00d 100644 --- a/composeApp/src/androidMain/AndroidManifest.xml +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -1,57 +1,2 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 39ae4c826..a9b1b5475 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,25 +1,26 @@ [versions] -agp = "8.13.2" -android-compileSdk = "36" +agp = "9.2.0" +android-compileSdk = "37" +android-compileSdkMinor = "0" android-minSdk = "24" android-targetSdk = "36" androidx-activity = "1.12.2" -androidx-navigation = "2.9.2" +androidx-navigation = "2.10.0-alpha02" androidx-appcompat = "1.7.1" androidx-core = "1.17.0" androidx-core-splashscreen = "1.0.1" androidx-espresso = "3.7.0" -androidx-lifecycle = "2.11.0-beta01" +androidx-lifecycle = "2.11.0-beta02" androidx-work = "2.10.3" androidx-testExt = "1.3.0" -composeMultiplatform = "1.11.1" +composeMultiplatform = "1.12.0-alpha02" coil = "3.5.0-beta01" kermit = "2.0.5" junit = "4.13.2" kotlin = "2.3.0" kotlinx-serialization = "1.8.1" ktor = "3.4.1" -material3 = "1.11.0-alpha07" +material3 = "1.12.0-alpha02" androidx-media3 = "1.8.0" supabase = "3.4.1" quickjsKt = "1.0.5" @@ -80,6 +81,7 @@ desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } androidLibrary = { id = "com.android.library", version.ref = "agp" } +androidKotlinMultiplatformLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" } composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d4081da47..c61a118f7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/settings.gradle.kts b/settings.gradle.kts index e08f97b53..884dc8e94 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,4 +28,5 @@ dependencyResolutionManagement { } } -include(":composeApp") \ No newline at end of file +include(":composeApp") +include(":androidApp")