- strings.xml 扩充至 ~470 条(原 3 条),全部 UI 硬编码中文迁移为资源引用 - 新增 UiText 抽象(Resource/Dynamic + asString()/resolve()),6 个 ViewModel 错误消息 UiText 化 - AppTips 13 条改为 @StringRes 资源 ID;TimeFilter/ChartGranularity/StatusStyle/DashboardViewModel 徽章改资源 ID - FormatUtils 保持纯函数,DurationText/标签参数注入(78 个纯 JVM 测试不受影响) - Widget XML(2) + WidgetProvider 文字资源化;品牌名 translatable=false - 开启 generateLocaleConfig + resources.properties(unqualifiedResLocale=zh-CN) - 修复审查发现的每周/每月行为回归(dashboard/widget 保持'每周/每月') - 删除死资源/死代码;ChartGranularity 粒度标签补 time_last_12h - 验证:compileDebugKotlin / testDebugUnitTest(78) / lintDebug / assembleDebug 全绿
231 lines
9.1 KiB
Plaintext
231 lines
9.1 KiB
Plaintext
import java.io.File
|
||
|
||
plugins {
|
||
alias(libs.plugins.android.application)
|
||
alias(libs.plugins.kotlin.compose)
|
||
alias(libs.plugins.kotlin.serialization)
|
||
alias(libs.plugins.ksp)
|
||
alias(libs.plugins.hilt.android)
|
||
}
|
||
|
||
android {
|
||
namespace = "com.rainy.token"
|
||
compileSdk = 35
|
||
|
||
defaultConfig {
|
||
applicationId = "com.rainy.token"
|
||
minSdk = 31
|
||
targetSdk = 35
|
||
versionCode = 7
|
||
versionName = "1.5.2"
|
||
|
||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||
vectorDrawables {
|
||
useSupportLibrary = true
|
||
}
|
||
}
|
||
|
||
signingConfigs {
|
||
create("release") {
|
||
// CI 环境没有 release.jks,自动 fallback 到 debug keystore
|
||
// Release workflow 通过 Secret 注入 release.jks
|
||
val keystoreFile = rootProject.file("release.jks")
|
||
if (keystoreFile.exists()) {
|
||
storeFile = keystoreFile
|
||
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: "RainyToken2026!"
|
||
keyAlias = System.getenv("KEYSTORE_ALIAS") ?: "rainy"
|
||
keyPassword = System.getenv("KEY_PASSWORD") ?: "RainyToken2026!"
|
||
} else {
|
||
// 仅在 CI 环境中 fallback 到 debug keystore(用于编译/资源完整性验证)
|
||
// 本地构建缺少 release.jks 时直接报错,避免静默生成 debug 签名的 Release APK
|
||
if (System.getenv("CI") != null) {
|
||
val debugKeystore = file("${System.getProperty("user.home")}/.android/debug.keystore")
|
||
storeFile = debugKeystore
|
||
storePassword = "android"
|
||
keyAlias = "androiddebugkey"
|
||
keyPassword = "android"
|
||
} else {
|
||
throw GradleException(
|
||
"release.jks 不存在,且当前不是 CI 环境。\n" +
|
||
"正式 Release 构建需要 release.jks 密钥库文件。\n" +
|
||
"如需本地验证编译,请设置环境变量 CI=true 或使用 assembleDebug。"
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
buildTypes {
|
||
debug {
|
||
isMinifyEnabled = false
|
||
}
|
||
release {
|
||
isMinifyEnabled = false
|
||
signingConfig = signingConfigs.getByName("release")
|
||
proguardFiles(
|
||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||
"proguard-rules.pro"
|
||
)
|
||
}
|
||
}
|
||
// noCompress workaround removed — no longer needed
|
||
compileOptions {
|
||
sourceCompatibility = JavaVersion.VERSION_17
|
||
targetCompatibility = JavaVersion.VERSION_17
|
||
}
|
||
buildFeatures {
|
||
compose = true
|
||
}
|
||
androidResources {
|
||
// Android 13+ 应用级语言设置:根据 values-* 目录自动生成 locales_config
|
||
generateLocaleConfig = true
|
||
}
|
||
packaging {
|
||
resources {
|
||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════
|
||
// AGP 9.0 ARM64 Proot: Release resource guard + fallback
|
||
//
|
||
// 根因:AGP 9.0 的 optimizeReleaseResources 传入 --resource-path-shortening-map=<path>
|
||
// 等号形式,ARM64 AAPT2 不接受此语法(只接受空格分隔),exit code=1 但 AGP 不检查,
|
||
// 导致 optimized .ap_ 缺失,packageRelease 生成无资源残缺 APK。
|
||
//
|
||
// 修复链:
|
||
// 1. ~/.gradle/gradle.properties → android.aapt2FromMavenOverride 指向 wrapper
|
||
// wrapper 将 --resource-path-shortening-map=<path> 拆分为两个独立 argv
|
||
// 2. 本任务作为构建防护:optimizeReleaseResources 之后验证 optimized .ap_ 完整性,
|
||
// 若缺失则复制 linked .ap_ 作为 fallback(功能正确,仅跳过 path shortening)
|
||
// 3. packageRelease 依赖此任务,确保消费到完整资源
|
||
// ═══════════════════════════════════════════════════════
|
||
if (System.getProperty("os.arch") == "aarch64") {
|
||
tasks.register("guardReleaseResources") {
|
||
dependsOn("optimizeReleaseResources")
|
||
doLast {
|
||
val linkedAp = layout.buildDirectory
|
||
.file("intermediates/linked_resources_binary_format/release/processReleaseResources/linked-resources-binary-format-release.ap_")
|
||
.get().asFile
|
||
val optimizedDir = layout.buildDirectory
|
||
.dir("intermediates/optimized_processed_res/release/optimizeReleaseResources")
|
||
.get().asFile
|
||
val optimizedAp = File(optimizedDir, "resources-release-optimize.ap_")
|
||
|
||
if (optimizedAp.exists() && optimizedAp.length() > 0) {
|
||
// optimized .ap_ 存在,验证内容完整性
|
||
val zipList = providers.exec {
|
||
commandLine("unzip", "-l", optimizedAp.absolutePath)
|
||
isIgnoreExitValue = true
|
||
}.standardOutput.asText.get()
|
||
|
||
val hasManifest = zipList.contains("AndroidManifest.xml")
|
||
val hasArsc = zipList.contains("resources.arsc")
|
||
val hasRes = zipList.contains("res/")
|
||
|
||
if (hasManifest && hasArsc && hasRes) {
|
||
logger.lifecycle("GuardReleaseResources: optimized .ap_ OK (${optimizedAp.length()} bytes)")
|
||
return@doLast
|
||
}
|
||
logger.warn("GuardReleaseResources: optimized .ap_ incomplete (manifest=$hasManifest arsc=$hasArsc res=$hasRes)")
|
||
} else {
|
||
logger.warn("GuardReleaseResources: optimized .ap_ missing or empty")
|
||
}
|
||
|
||
// Fallback: copy linked .ap_ → optimized .ap_
|
||
if (!linkedAp.exists() || linkedAp.length() == 0L) {
|
||
throw GradleException("GuardReleaseResources: linked .ap_ also missing or empty — cannot recover")
|
||
}
|
||
|
||
optimizedDir.mkdirs()
|
||
linkedAp.copyTo(optimizedAp, overwrite = true)
|
||
|
||
// Verify the copy
|
||
val verifyList = providers.exec {
|
||
commandLine("unzip", "-l", optimizedAp.absolutePath)
|
||
isIgnoreExitValue = true
|
||
}.standardOutput.asText.get()
|
||
|
||
val vManifest = verifyList.contains("AndroidManifest.xml")
|
||
val vArsc = verifyList.contains("resources.arsc")
|
||
val vRes = verifyList.contains("res/")
|
||
|
||
if (!vManifest || !vArsc || !vRes) {
|
||
throw GradleException("GuardReleaseResources: fallback copy verification failed (manifest=$vManifest arsc=$vArsc res=$vRes)")
|
||
}
|
||
|
||
logger.lifecycle("GuardReleaseResources: fallback — copied linked .ap_ → optimized .ap_ (${optimizedAp.length()} bytes, path shortening skipped)")
|
||
}
|
||
}
|
||
|
||
// packageRelease 必须依赖此防护任务
|
||
project.tasks.matching { it.name == "packageRelease" }.configureEach {
|
||
dependsOn("guardReleaseResources")
|
||
}
|
||
}
|
||
|
||
// Force ARM64 AAPT2 in Proot environment (local only; GitHub Actions x86_64 uses default)
|
||
if (System.getProperty("os.arch") == "aarch64") {
|
||
configurations.all {
|
||
resolutionStrategy.eachDependency {
|
||
if (requested.group == "com.android.tools.build" && requested.name == "aapt2") {
|
||
useTarget("com.android.tools.build:aapt2:${'$'}{requested.version}:linux-aarch64")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
dependencies {
|
||
|
||
// Core
|
||
implementation(libs.androidx.core.ktx)
|
||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||
implementation(libs.androidx.activity.compose)
|
||
|
||
// Compose
|
||
implementation(platform(libs.androidx.compose.bom))
|
||
implementation(libs.androidx.ui)
|
||
implementation(libs.androidx.ui.graphics)
|
||
implementation(libs.androidx.ui.tooling.preview)
|
||
implementation(libs.androidx.material3)
|
||
implementation(libs.androidx.material3.window.size)
|
||
|
||
// Navigation
|
||
implementation(libs.androidx.navigation.compose)
|
||
|
||
// Network
|
||
implementation(libs.retrofit)
|
||
implementation(libs.okhttp)
|
||
implementation(libs.okhttp.logging.interceptor)
|
||
implementation(libs.kotlinx.serialization.json)
|
||
implementation(libs.retrofit.kotlinx.serialization.converter)
|
||
|
||
// DataStore
|
||
implementation(libs.androidx.datastore.preferences)
|
||
|
||
// WebKit
|
||
implementation(libs.androidx.webkit)
|
||
|
||
// DI (Hilt + KSP)
|
||
implementation(libs.hilt.android)
|
||
ksp(libs.hilt.compiler)
|
||
implementation(libs.androidx.hilt.navigation.compose)
|
||
|
||
// Room
|
||
implementation(libs.room.runtime)
|
||
implementation(libs.room.ktx)
|
||
ksp(libs.room.compiler)
|
||
|
||
// Test
|
||
testImplementation(libs.junit)
|
||
androidTestImplementation(libs.androidx.junit)
|
||
androidTestImplementation(libs.androidx.espresso.core)
|
||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||
debugImplementation(libs.androidx.ui.tooling)
|
||
debugImplementation(libs.androidx.ui.test.manifest)
|
||
}
|