plugins { alias(libs.plugins.androidApplication) id 'org.jetbrains.kotlin.android' version '1.9.22' } def localProps = new Properties() def localPropsFile = rootProject.file('local.properties') if (localPropsFile.exists()) { localPropsFile.withInputStream { localProps.load(it) } } static def escapeStringLiteral(String value) { if (value == null) return '""' return '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"' } def buildConfigString = { String key -> escapeStringLiteral(localProps.getProperty(key, '')) } // Boolean config: anything case-insensitive-matching "true"/"1"/"yes" counts. def buildConfigBool = { String key -> def raw = localProps.getProperty(key, '').trim().toLowerCase() (raw == 'true' || raw == '1' || raw == 'yes') ? 'true' : 'false' } android { namespace 'net.kuafuai.andee' compileSdk 34 buildFeatures { // AGP 8 默认关掉了。开出来就能用 BuildConfig.XXX。 buildConfig true } defaultConfig { applicationId "net.kuafuai.andee" minSdk 30 targetSdk 34 versionCode 9 versionName "0.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // The CodeFlying mode: a hosted backend that proxies LLM, ASR and TTS // from one domain. When ENABLED and DOMAIN is set, "codeflying" is the // out-of-box backend choice; URL scheme follows SSL_ENABLED // (ws/wss, http/https). Everything is derived at runtime from these // four fields — the user does not configure the endpoints themselves. buildConfigField "boolean", "CODEFLYING_ENABLED", buildConfigBool('CODEFLYING_ENABLED') buildConfigField "String", "CODEFLYING_KEY", buildConfigString('CODEFLYING_KEY') buildConfigField "String", "CODEFLYING_DOMAIN", buildConfigString('CODEFLYING_DOMAIN') buildConfigField "boolean", "CODEFLYING_SSL_ENABLED", buildConfigBool('CODEFLYING_SSL_ENABLED') buildConfigField "String", "DEFAULT_LANG", buildConfigString('DEFAULT_LANG') buildConfigField "String", "DEFAULT_THINKING", buildConfigString('DEFAULT_THINKING') // The Google voice path, baked in at package time. // // Same rule as CODEFLYING_KEY above and for the same reason: a key in // the source is a key in a search index, so it lives in // local.properties — which .gitignore excludes — and the repository's // copy of that file stays empty. The source's own default is the empty // string, so a checkout with nothing configured builds exactly what it // built before Google existed. // // **GOOGLE_VOICE_KEY non-empty is also what makes 谷歌 the default // provider** — see VoiceConfig.voiceProvider. A build is packaged for // whoever it is packaged for, and baking a key is the most explicit // statement of that intent there is. buildConfigField "String", "GOOGLE_VOICE_KEY", buildConfigString('GOOGLE_VOICE_KEY') buildConfigField "String", "GOOGLE_VOICE_PROJECT", buildConfigString('GOOGLE_VOICE_PROJECT') buildConfigField "String", "GOOGLE_VOICE_LOCATION", buildConfigString('GOOGLE_VOICE_LOCATION') buildConfigField "String", "GOOGLE_VOICE_BASE_URL", buildConfigString('GOOGLE_VOICE_BASE_URL') buildConfigField "String", "GOOGLE_VOICE_ASR_MODEL", buildConfigString('GOOGLE_VOICE_ASR_MODEL') buildConfigField "String", "GOOGLE_VOICE_TTS_MODEL", buildConfigString('GOOGLE_VOICE_TTS_MODEL') // Verification hook, off unless asked for: `-PdbgSuffix=.dbg` builds a // *parallel* package that installs next to whatever is already on the // device. Needed because the package on the tablet was signed with a // different key than this checkout, so `pm install -r` is refused // (INSTALL_FAILED_UPDATE_INCOMPATIBLE) and the only alternative is // uninstalling — which throws away the user's chat history, notebook // and vault. Not for shipping; nothing else reads this property. if (project.hasProperty('dbgSuffix')) { applicationIdSuffix project.property('dbgSuffix') } } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } // One APK per ABI instead of one fat APK carrying all four. ML Kit's // barhopper .so alone is ~4 MB per ABI, so the fat APK paid that four // times over. x86 (32-bit) is deliberately dropped: no emulator has needed // it for years. x86_64 stays so an emulator still installs something. // // `universalApk false` on purpose: a fat APK sitting next to the split ones // is a trap — it is the file someone grabs when they are in a hurry, and it // is four times the size. Anything the splits do not cover is a device this // app cannot be provisioned on anyway. splits { abi { enable true reset() include 'arm64-v8a', 'armeabi-v7a', 'x86_64' universalApk false } } // Native libs ship deflated rather than page-aligned-and-stored. AGP 8 // defaults to stored (`extractNativeLibs="false"`), which is the better // trade for a store install — the libs are mapped straight out of the APK // with no second copy on disk. This app is side-loaded instead, where the // size of the file being handed over is the thing that matters, and // barhopper deflates 4.2 MB → 1.8 MB. The cost is ~4 MB of extra on-device // storage per install and a slightly slower install. Flip to false if that // trade ever stops being the right one. packaging { jniLibs { useLegacyPackaging = true } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } } dependencies { implementation libs.appcompat implementation libs.material implementation 'org.java-websocket:Java-WebSocket:1.5.7' implementation 'com.squareup.okhttp3:okhttp:4.12.0' // The robot dog's dongle is a plain USB-serial device (Arduino Nano: // CH340/CH9102/FTDI depending on the clone). 3.8.1 deliberately, not the // latest: 3.11.0 pulls kotlin-stdlib 2.2.x, whose metadata this project's // Kotlin 1.9.22 compiler cannot read. 3.8.1 is pure Java. implementation 'com.github.mik3y:usb-serial-for-android:3.8.1' // Camera eye: CameraX preview/analysis + ML Kit barcode. def camerax_version = "1.3.4" implementation "androidx.camera:camera-core:$camerax_version" implementation "androidx.camera:camera-camera2:$camerax_version" implementation "androidx.camera:camera-lifecycle:$camerax_version" implementation "androidx.camera:camera-view:$camerax_version" implementation 'com.google.mlkit:barcode-scanning:17.2.0' testImplementation libs.junit // Unit tests run against android.jar's stubs, whose org.json throws // "not mocked" on construction. BrainTrace builds a JSONObject before it // can know tracing is off, so any test that reaches it needs a real // implementation on the unit-test classpath. Test scope only: the app still // serializes with Android's own org.json, and no test asserts on the JSON. testImplementation 'org.json:json:20240303' androidTestImplementation libs.ext.junit androidTestImplementation libs.espresso.core }