import com.android.build.api.dsl.ProductFlavor import com.google.protobuf.gradle.id import com.mikepenz.aboutlibraries.plugin.DuplicateMode import com.mikepenz.aboutlibraries.plugin.DuplicateRule import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.util.Base64 import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.ksp) alias(libs.plugins.kotlin.compose) alias(libs.plugins.hilt) alias(libs.plugins.room) alias(libs.plugins.protobuf) alias(libs.plugins.kotlin.plugin.serialization) alias(libs.plugins.aboutLibraries) alias(libs.plugins.openapi.generator) } val isCI = if (System.getenv("CI") != null) System.getenv("CI").toBoolean() else false val shouldSign = isCI && System.getenv("KEY_ALIAS") != null val ffmpegModuleExists = project.file("libs/lib-decoder-ffmpeg-release.aar").exists() val av1ModuleExists = project.file("libs/lib-decoder-av1-release.aar").exists() val mpvModuleExists = project.file("libs/wholphin-mpv-release.aar").exists() val extensionsRepoActive = project.hasProperty("WholphinExtensionsUsername") val gitTags = providers .exec { commandLine("git", "tag", "--list", "v*", "p*") } .standardOutput.asText .get() val gitDescribe = providers .exec { commandLine("git", "describe", "--tags", "--long", "--match=v*") } .standardOutput.asText .getOrElse("v0.0.0") android { namespace = "com.github.damontecres.wholphin" compileSdk = 36 defaultConfig { applicationId = "com.github.damontecres.wholphin" minSdk = 23 targetSdk = 36 versionCode = gitTags.trim().lines().size versionName = gitDescribe.trim().removePrefix("v").ifBlank { "0.0.0" } testInstrumentationRunner = "com.github.damontecres.wholphin.test.WholphinTestRunner" buildConfigField("long", "BUILD_TIME", System.currentTimeMillis().toString()) } signingConfigs { if (shouldSign) { create("ci") { file("ci.keystore").writeBytes( Base64.getDecoder().decode(System.getenv("SIGNING_KEY")), ) keyAlias = System.getenv("KEY_ALIAS") keyPassword = System.getenv("KEY_PASSWORD") storePassword = System.getenv("KEY_STORE_PASSWORD") storeFile = file("ci.keystore") enableV1Signing = true enableV2Signing = true enableV3Signing = true enableV4Signing = true } } } buildTypes { release { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) isDebuggable = false if (shouldSign) { signingConfig = signingConfigs.getByName("ci") } else { val localPropertiesFile = project.rootProject.file("local.properties") if (localPropertiesFile.exists()) { val properties = Properties() properties.load(localPropertiesFile.inputStream()) val signingConfigName = properties["release.signing.config"]?.toString() if (signingConfigName != null) { signingConfig = signingConfigs.getByName(signingConfigName) } } } } debug { isMinifyEnabled = false isDebuggable = true applicationIdSuffix = ".debug" } applicationVariants.all { val variant = this variant.outputs .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } .forEach { output -> val abi = output.getFilter("ABI").let { if (it != null) "-$it" else "" } val outputFileName = "Wholphin-${variant.baseName}-${variant.versionName}-${variant.versionCode}$abi.apk" output.outputFileName = outputFileName } } } flavorDimensions += "version" productFlavors { val featureLeanback = "leanback" val featureUpdate = "UPDATING_ENABLED" val featureDiscover = "DISCOVER_ENABLED" fun ProductFlavor.setFeatureFlag( name: String, enabled: Boolean, ) { this.buildConfigField("boolean", name, "Boolean.parseBoolean(\"${enabled}\")") } create("default") { dimension = "version" isDefault = true manifestPlaceholders += mapOf(featureLeanback to false) setFeatureFlag(featureUpdate, true) setFeatureFlag(featureDiscover, true) } create("appstore") { dimension = "version" manifestPlaceholders += mapOf(featureLeanback to true) setFeatureFlag(featureUpdate, false) setFeatureFlag(featureDiscover, true) } create("firetv") { dimension = "version" manifestPlaceholders += mapOf(featureLeanback to true) setFeatureFlag(featureUpdate, false) setFeatureFlag(featureDiscover, false) } } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 isCoreLibraryDesugaringEnabled = true } kotlin { compilerOptions { languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_3 jvmTarget = JvmTarget.JVM_11 javaParameters = true } } buildFeatures { buildConfig = true compose = true } room { schemaDirectory("$projectDir/schemas") } splits { abi { isEnable = true reset() include("armeabi-v7a", "arm64-v8a") isUniversalApk = true } } packaging { jniLibs { // Work around because libass-android & wholphin-mpv both (incorrectly) package libc++_shared.so pickFirsts += "lib/*/libc++_shared.so" } } sourceSets { getByName("main") { kotlin.directories += "$buildDir/generated/seerr_api/src/main/kotlin" } } testOptions { unitTests { isIncludeAndroidResources = true } } lint { disable.add("MissingTranslation") } } protobuf { protoc { artifact = "com.google.protobuf:protoc:${libs.protobuf.kotlin.lite.get().version}" } generateProtoTasks { all().forEach { it.plugins { id("java") { option("lite") } } it.builtins { id("kotlin") { option("lite") } } } } } aboutLibraries { collect { configPath = file("config") } library { duplicationMode = DuplicateMode.MERGE duplicationRule = DuplicateRule.SIMPLE } } openApiGenerate { generatorName.set("kotlin") inputSpec.set("$projectDir/src/main/seerr/seerr-api.yml") templateDir.set("$projectDir/src/main/seerr/templates") outputDir.set("$buildDir/generated/seerr_api") apiPackage.set("com.github.damontecres.wholphin.api.seerr") modelPackage.set("com.github.damontecres.wholphin.api.seerr.model") groupId.set("com.github.damontecres.wholphin.api.seerr") id.set("seerr-api") packageName.set("com.github.damontecres.wholphin.api.seerr") additionalProperties.apply { put("serializationLibrary", "kotlinx_serialization") put("sortModelPropertiesByRequiredFlag", true) put("sortParamsByRequiredFlag", true) put("useCoroutines", true) put("enumPropertyNaming", "UPPERCASE") put("modelMutable", false) // Note: this is only for downloading files, so it's not necessary to enable put("supportAndroidApiLevel25AndBelow", false) } } tasks.named("preBuild") { dependsOn.add(tasks.named("openApiGenerate")) } dependencies { implementation(libs.androidx.core.ktx) implementation(libs.androidx.appcompat) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.graphics) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.tv.foundation) implementation(libs.androidx.tv.material) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.activity.compose) implementation(libs.androidx.datastore) implementation(libs.androidx.datastore.preferences) implementation(libs.protobuf.kotlin.lite) implementation(libs.androidx.tvprovider) implementation(libs.androidx.work.runtime.ktx) implementation(libs.androidx.hilt.work) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.session) implementation(libs.androidx.media3.datasource.okhttp) implementation(libs.androidx.media3.exoplayer.hls) implementation(libs.androidx.media3.exoplayer.dash) implementation(libs.androidx.media3.ui) implementation(libs.androidx.media3.ui.compose) implementation(libs.ass.media) implementation(libs.coil.core) implementation(libs.coil.compose) implementation(libs.coil.network.cachecontrol) implementation(libs.coil.network.okhttp) implementation(libs.coil.gif) implementation(libs.coil.svg) implementation(libs.jellyfin.core) implementation(libs.jellyfin.api) implementation(libs.jellyfin.api.okhttp) implementation(libs.androidx.navigation3.ui) implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.material3.adaptive.navigation3) implementation(libs.kotlinx.serialization.core) implementation(libs.kotlinx.serialization.json) implementation(libs.hilt.android) implementation(libs.androidx.room.common.jvm) implementation(libs.androidx.room.ktx) implementation(libs.androidx.compose.material3) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.hilt.navigation.compose) implementation(libs.androidx.preference.ktx) implementation(libs.androidx.room.testing) implementation(libs.androidx.palette.ktx) implementation(libs.androidx.media3.effect) implementation(libs.androidx.runner) ksp(libs.androidx.room.compiler) ksp(libs.hilt.android.compiler) ksp(libs.androidx.hilt.compiler) implementation(libs.timber) implementation(libs.slf4j2.timber) implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) implementation(libs.multiplatform.markdown.renderer) implementation(libs.multiplatform.markdown.renderer.m3) implementation(libs.programguide) implementation(libs.acra.http) implementation(libs.acra.dialog) implementation(libs.acra.limiter) compileOnly(libs.auto.service.annotations) ksp(libs.auto.service.ksp) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp) implementation(libs.kache) implementation(libs.kache.file) androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.test.manifest) coreLibraryDesugaring(libs.desugar.jdk.libs) if (ffmpegModuleExists) { logger.info("Using local ffmpeg decoder") implementation(files("libs/lib-decoder-ffmpeg-release.aar")) } else if (extensionsRepoActive) { logger.info("Using prebuilt ffmpeg decoder") implementation(libs.wholphin.extensions.ffmpeg) } else { logger.warn("Media3 ffmpeg decoder was NOT found") } if (av1ModuleExists) { logger.info("Using local av1 decoder") implementation(files("libs/lib-decoder-av1-release.aar")) } else if (extensionsRepoActive) { logger.info("Using prebuilt av1 decoder") implementation(libs.wholphin.extensions.av1) } else { logger.warn("Media3 av1 decoder was NOT found") } if (mpvModuleExists) { logger.info("Using local libMPV build") implementation(files("libs/wholphin-mpv-release.aar")) } else if (extensionsRepoActive) { logger.info("Using prebuilt libMPV") implementation(libs.wholphin.extensions.mpv) } else { logger.warn("libMPV was NOT found") } testImplementation(libs.mockk.android) testImplementation(libs.mockk.agent) testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.androidx.core.testing) testImplementation(libs.robolectric) testImplementation(libs.hilt.android.testing) testImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.mockk.android) androidTestImplementation(libs.hilt.android.testing) androidTestImplementation(libs.androidx.ui.test.manifest) }