d3ro-voice/apps/mobile-rn/android/app/build.gradle
Yun Chan 57c17d0977
Some checks failed
deploy-site / deploy (push) Failing after 13m48s
fix(desktop): recommend the model the app actually uses
The Ollama setup guide still led with two retired models while the app already
defaults to the newer one, so a fresh setup would install a model the app does
not use. The guide now recommends the same model as the rest of the app and
lists current lightweight alternatives.
2026-09-18 17:15:37 +09:00

411 lines
16 KiB
Groovy

apply plugin: "com.android.application"
apply plugin: "org.jetbrains.kotlin.android"
apply plugin: "com.facebook.react"
abstract class VerifyD3roMobileVariantConfiguration extends DefaultTask {
private static final String GOOGLE_TEST_ADMOB_PUBLISHER_ID = "ca-app-pub-3940256099942544"
private static final java.util.regex.Pattern STRICT_SEMVER = java.util.regex.Pattern.compile(
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
)
@org.gradle.api.tasks.Input
abstract org.gradle.api.provider.Property<String> getVariantName()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getVersionNameSetting()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getVersionCodeSetting()
@org.gradle.api.tasks.Input
abstract org.gradle.api.provider.ListProperty<String> getMissingReleaseSettings()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getFirebaseConfigPath()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getReleaseStoreFilePath()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getProductionAdMobAppId()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getProductionBannerUnitId()
@org.gradle.api.tasks.Input
@org.gradle.api.tasks.Optional
abstract org.gradle.api.provider.Property<String> getProductionRewardedUnitId()
@org.gradle.api.tasks.TaskAction
void verifyConfiguration() {
def versionName = versionNameSetting.orNull
def versionCode = versionCodeSetting.orNull
if (versionName == null || versionName.trim().isEmpty()) {
throw new GradleException("D3RO_VERSION_NAME is required for release and e2e builds")
}
if (versionName.length() > 100 || !STRICT_SEMVER.matcher(versionName).matches()) {
throw new GradleException("D3RO_VERSION_NAME must be a strict semantic version")
}
if (versionCode == null || !(versionCode ==~ /[1-9]\d{0,9}/)) {
throw new GradleException("D3RO_VERSION_CODE must be a positive decimal integer")
}
if (versionCode.toLong() > 2100000000L) {
throw new GradleException("D3RO_VERSION_CODE exceeds the Android maximum")
}
if (variantName.get() != "release") {
return
}
if (!new File(firebaseConfigPath.get()).isFile()) {
throw new GradleException(
"Release build is blocked. Add android/app/google-services.json for com.d3ro.voice"
)
}
def missingSettings = missingReleaseSettings.get()
if (!missingSettings.isEmpty()) {
throw new GradleException(
"Release build is blocked. Configure: ${missingSettings.join(', ')}"
)
}
if (!new File(releaseStoreFilePath.get()).isFile()) {
throw new GradleException("D3RO_RELEASE_STORE_FILE must point to an existing keystore")
}
def adMobAppId = productionAdMobAppId.get()
if (!(adMobAppId ==~ /ca-app-pub-\d+~\d+/)) {
throw new GradleException("D3RO_ADMOB_APP_ID has an invalid format")
}
if (adMobAppId.startsWith(GOOGLE_TEST_ADMOB_PUBLISHER_ID + "~")) {
throw new GradleException(
"D3RO_ADMOB_APP_ID must not use Google's official test publisher"
)
}
[
D3RO_ADMOB_BANNER_UNIT_ID: productionBannerUnitId.get(),
D3RO_ADMOB_REWARDED_UNIT_ID: productionRewardedUnitId.get(),
].each { name, value ->
if (!(value ==~ /ca-app-pub-\d+\/\d+/)) {
throw new GradleException("${name} has an invalid format")
}
if (value.startsWith(GOOGLE_TEST_ADMOB_PUBLISHER_ID + "/")) {
throw new GradleException(
"${name} must not use Google's official test publisher"
)
}
}
}
}
def firebaseConfigFile = file("google-services.json")
if (firebaseConfigFile.isFile()) {
apply plugin: "com.google.gms.google-services"
}
def secureSetting = { String name ->
def gradleValue = providers.gradleProperty(name)
if (gradleValue.present) {
return gradleValue.get()
}
def environmentValue = providers.environmentVariable(name)
return environmentValue.present ? environmentValue.get() : null
}
def releaseStoreFilePath = secureSetting("D3RO_RELEASE_STORE_FILE")
def releaseStorePassword = secureSetting("D3RO_RELEASE_STORE_PASSWORD")
def releaseKeyAlias = secureSetting("D3RO_RELEASE_KEY_ALIAS")
def releaseKeyPassword = secureSetting("D3RO_RELEASE_KEY_PASSWORD")
def releaseSigningReady = [
releaseStoreFilePath,
releaseStorePassword,
releaseKeyAlias,
releaseKeyPassword,
].every { it != null && !it.trim().isEmpty() }
def productionAdMobAppId = secureSetting("D3RO_ADMOB_APP_ID")
def productionBannerUnitId = secureSetting("D3RO_ADMOB_BANNER_UNIT_ID")
def productionRewardedUnitId = secureSetting("D3RO_ADMOB_REWARDED_UNIT_ID")
def configuredVersionName = secureSetting("D3RO_VERSION_NAME")
def configuredVersionCode = secureSetting("D3RO_VERSION_CODE")
def e2eSupabaseUrlOverride = secureSetting("D3RO_E2E_SUPABASE_URL") ?: ""
def e2eSupabaseAnonKeyOverride = secureSetting("D3RO_E2E_SUPABASE_ANON_KEY") ?: ""
def e2eSupabaseOverridePresent = !e2eSupabaseUrlOverride.isEmpty() || !e2eSupabaseAnonKeyOverride.isEmpty()
def e2eSupabaseOverrideValid = e2eSupabaseUrlOverride == "http://10.0.2.2:55321" &&
e2eSupabaseAnonKeyOverride ==~ /sb_publishable_[A-Za-z0-9_-]{20,}/
if (e2eSupabaseOverridePresent && !e2eSupabaseOverrideValid) {
throw new GradleException(
"Local mobile E2E Supabase override must use the exact emulator endpoint and a publishable key"
)
}
def strictSemver = ~/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/
def configuredVersionCodeValue = configuredVersionCode != null &&
configuredVersionCode ==~ /[1-9]\d{0,9}/ ? configuredVersionCode.toLong() : null
def versionSettingsValid = configuredVersionName != null &&
configuredVersionName.length() <= 100 &&
configuredVersionName ==~ strictSemver &&
configuredVersionCodeValue != null &&
configuredVersionCodeValue <= 2100000000L
def resolvedVersionName = versionSettingsValid ? configuredVersionName : "1.3.5"
def resolvedVersionCode = versionSettingsValid ? configuredVersionCodeValue.toInteger() : 1031005
def requiredReleaseSettings = [
D3RO_RELEASE_STORE_FILE: releaseStoreFilePath,
D3RO_RELEASE_STORE_PASSWORD: releaseStorePassword,
D3RO_RELEASE_KEY_ALIAS: releaseKeyAlias,
D3RO_RELEASE_KEY_PASSWORD: releaseKeyPassword,
D3RO_ADMOB_APP_ID: productionAdMobAppId,
D3RO_ADMOB_BANNER_UNIT_ID: productionBannerUnitId,
D3RO_ADMOB_REWARDED_UNIT_ID: productionRewardedUnitId,
]
def missingReleaseSettingNames = requiredReleaseSettings.findAll {
it.value == null || it.value.trim().isEmpty()
}.keySet().toList().sort()
def releaseStoreAbsolutePath = releaseStoreFilePath == null || releaseStoreFilePath.trim().isEmpty()
? null
: file(releaseStoreFilePath).absolutePath
def releaseAdMobAppIdValue = productionAdMobAppId
def releaseBannerUnitIdValue = productionBannerUnitId
def releaseRewardedUnitIdValue = productionRewardedUnitId
def configureVersionGate = { VerifyD3roMobileVariantConfiguration gate ->
if (configuredVersionName != null) {
gate.versionNameSetting.set(configuredVersionName)
}
if (configuredVersionCode != null) {
gate.versionCodeSetting.set(configuredVersionCode)
}
gate.missingReleaseSettings.set([])
}
def verifyE2eBuildConfiguration = tasks.register(
"verifyE2eBuildConfiguration",
VerifyD3roMobileVariantConfiguration
) {
group = "verification"
description = "Fails closed unless the e2e version contract is configured."
variantName.set("e2e")
configureVersionGate(delegate)
}
def verifyReleaseBuildConfiguration = tasks.register(
"verifyReleaseBuildConfiguration",
VerifyD3roMobileVariantConfiguration
) {
group = "verification"
description = "Fails closed unless all production release settings are configured."
variantName.set("release")
configureVersionGate(delegate)
missingReleaseSettings.set(missingReleaseSettingNames)
firebaseConfigPath.set(firebaseConfigFile.absolutePath)
if (releaseStoreAbsolutePath != null) {
releaseStoreFilePath.set(releaseStoreAbsolutePath)
}
if (releaseAdMobAppIdValue != null) {
productionAdMobAppId.set(releaseAdMobAppIdValue)
}
if (releaseBannerUnitIdValue != null) {
productionBannerUnitId.set(releaseBannerUnitIdValue)
}
if (releaseRewardedUnitIdValue != null) {
productionRewardedUnitId.set(releaseRewardedUnitIdValue)
}
}
// Wire gates to actual Android variant lifecycle tasks. Aggregate invocations
// such as `assemble` and `build` cannot bypass these dependencies merely because
// their requested task names omit the build type.
tasks.configureEach { candidate ->
if (candidate.name == "preE2eBuild") {
candidate.dependsOn(verifyE2eBuildConfiguration)
} else if (candidate.name == "preReleaseBuild") {
candidate.dependsOn(verifyReleaseBuildConfiguration)
}
}
def debugAdMobAppId = "ca-app-pub-3940256099942544~3347511713"
def debugBannerUnitId = "ca-app-pub-3940256099942544/6300978111"
def debugRewardedUnitId = "ca-app-pub-3940256099942544/5224354917"
def whisperModelFile = file("src/main/assets/models/ggml-tiny.bin")
def whisperModelSize = 77691713L
def whisperModelSha256 = "be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21"
def verifyWhisperModel = tasks.register("verifyWhisperModel") {
inputs.file(whisperModelFile)
doLast {
if (!whisperModelFile.isFile()) {
throw new GradleException("Missing bundled Whisper model: ${whisperModelFile}")
}
if (whisperModelFile.length() != whisperModelSize) {
throw new GradleException("Bundled Whisper model has an invalid size")
}
def digest = java.security.MessageDigest.getInstance("SHA-256")
whisperModelFile.withInputStream { stream ->
byte[] buffer = new byte[1024 * 1024]
int read
while ((read = stream.read(buffer)) > 0) {
digest.update(buffer, 0, read)
}
}
def actualSha256 = digest.digest().collect {
String.format("%02x", it & 0xff)
}.join()
if (actualSha256 != whisperModelSha256) {
throw new GradleException("Bundled Whisper model checksum verification failed")
}
}
}
tasks.named("preBuild").configure {
dependsOn(verifyWhisperModel)
}
def asBuildConfigString = { String value ->
return "\"${(value ?: '').replace('\\', '\\\\').replace('\"', '\\\"')}\""
}
react {
autolinkLibrariesWithApp()
// Only the developer debug variant relies on Metro. The signed `e2e`
// variant below must embed its Hermes bundle for clean-install emulator
// testing and CI cold starts.
debuggableVariants = ["debug"]
}
def enableProguardInReleaseBuilds = false
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
compileSdk rootProject.ext.compileSdkVersion
namespace "com.d3ro.voice"
buildFeatures {
buildConfig true
}
androidResources {
noCompress += ["bin"]
}
defaultConfig {
applicationId "com.d3ro.voice"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode resolvedVersionCode
versionName resolvedVersionName
manifestPlaceholders = [
usesCleartextTraffic: "false",
d3roAdMobAppId: debugAdMobAppId,
]
buildConfigField "boolean", "FIREBASE_CONFIGURED", firebaseConfigFile.isFile().toString()
buildConfigField "boolean", "E2E_TEST_BUILD", "false"
buildConfigField "String", "SUPABASE_URL_OVERRIDE", asBuildConfigString("")
buildConfigField "String", "SUPABASE_ANON_KEY_OVERRIDE", asBuildConfigString("")
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
if (releaseSigningReady) {
release {
storeFile file(releaseStoreFilePath)
storePassword releaseStorePassword
keyAlias releaseKeyAlias
keyPassword releaseKeyPassword
v1SigningEnabled true
v2SigningEnabled true
enableV3Signing = true
enableV4Signing = true
}
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
manifestPlaceholders = [
usesCleartextTraffic: "true",
d3roAdMobAppId: debugAdMobAppId,
]
buildConfigField "String", "ADMOB_BANNER_UNIT_ID", asBuildConfigString(debugBannerUnitId)
buildConfigField "String", "ADMOB_REWARDED_UNIT_ID", asBuildConfigString(debugRewardedUnitId)
ndk {
abiFilters "arm64-v8a", "x86_64"
}
}
e2e {
initWith(buildTypes.debug)
debuggable false
signingConfig signingConfigs.debug
// A non-debuggable E2E app is compiled as RelWithDebInfo. Keep
// native dependency variants on the same NDEBUG/props ABI.
matchingFallbacks = ["release"]
manifestPlaceholders = [
usesCleartextTraffic: e2eSupabaseOverrideValid ? "true" : "false",
d3roAdMobAppId: debugAdMobAppId,
]
buildConfigField "boolean", "E2E_TEST_BUILD", "true"
buildConfigField "String", "SUPABASE_URL_OVERRIDE", asBuildConfigString(e2eSupabaseUrlOverride)
buildConfigField "String", "SUPABASE_ANON_KEY_OVERRIDE", asBuildConfigString(e2eSupabaseAnonKeyOverride)
buildConfigField "String", "ADMOB_BANNER_UNIT_ID", asBuildConfigString(debugBannerUnitId)
buildConfigField "String", "ADMOB_REWARDED_UNIT_ID", asBuildConfigString(debugRewardedUnitId)
ndk {
abiFilters.clear()
abiFilters.add("arm64-v8a")
abiFilters.add("x86_64")
}
}
release {
if (releaseSigningReady) {
signingConfig signingConfigs.release
}
manifestPlaceholders = [
usesCleartextTraffic: "false",
d3roAdMobAppId: productionAdMobAppId ?: debugAdMobAppId,
]
buildConfigField "String", "ADMOB_BANNER_UNIT_ID", asBuildConfigString(productionBannerUnitId)
buildConfigField "String", "ADMOB_REWARDED_UNIT_ID", asBuildConfigString(productionRewardedUnitId)
ndk {
abiFilters "arm64-v8a"
}
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
androidComponents {
onVariants(selector().withBuildType("release")) { variant ->
variant.packaging.jniLibs.excludes.add("**/x86_64/**")
}
}
dependencies {
implementation("com.facebook.react:react-android")
implementation platform("com.google.firebase:firebase-bom:34.17.0")
implementation("com.google.firebase:firebase-messaging")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
androidTestImplementation("androidx.test:runner:1.7.0")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
}