diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index a6d5269d3..ca5a5bb3b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,8 +31,8 @@ jobs: java-version: '21' distribution: 'temurin' - # lintAll gates :proguard-tests, which applies the google-services plugin and - # will not configure without this file. Mirrors what scripts/build.sh copies. + # lintAll gates :app and :proguard-tests, both of which apply the google-services + # plugin and will not configure without this file. Mirrors what build.sh copies. - name: Copy google-services.json run: | cp library/google-services.json app/google-services.json diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7870c500..75b67e45c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -39,6 +39,26 @@ android { } } } + + lint { + // Module specific + disable += mutableSetOf( + // Reads the root wrapper, but only the application module analyses it, so it + // belongs here rather than in the shared policy. For reproducible builds. + "AndroidGradlePluginVersion", + // The demos log their auth callbacks unconditionally on purpose — watching + // logcat is how you see one fire. Unlike :auth's, none of these log user data. + "LogConditional", + // Glide's KSP processor does not generate GlideApp, which the storage demo and + // storage/README.md are both written around. Migration tracked separately. + "KaptUsageInsteadOfKsp", + // A themed icon needs a flat silhouette drawn for the purpose. The only + // candidate here is ic_launcher_foreground, whose opaque region is a solid + // plate, so it tints to a featureless block — worse than no monochrome layer. + "MonochromeLauncherIcon" + ) + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 77abc5776..9eb4b0fc3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,6 @@ android:usesCleartextTraffic="true"> @@ -22,7 +21,13 @@ - + + diff --git a/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt index 4e99cbfd3..b756c2276 100644 --- a/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/database/DatabaseDemoActivity.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.firebase.ui.database.paging.DatabasePagingOptions import com.firebase.ui.database.paging.FirebaseRecyclerPagingAdapter +import com.firebaseui.android.demo.R import com.google.firebase.auth.FirebaseAuth import com.google.firebase.database.DatabaseReference import com.google.firebase.database.FirebaseDatabase @@ -122,7 +123,8 @@ class ScoreAdapter(options: DatabasePagingOptions) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ScoreViewHolder(parent) override fun onBindViewHolder(holder: ScoreViewHolder, position: Int, model: ScoreItem) { - (holder.itemView as TextView).text = "${model.name} — score: ${model.score}" + val row = holder.itemView as TextView + row.text = row.context.getString(R.string.demo_score_row, model.name, model.score) } } diff --git a/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt b/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt index 91d07abb9..3fb4e44d5 100644 --- a/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt +++ b/app/src/main/java/com/firebaseui/android/demo/firestore/FirestoreDemoActivity.kt @@ -33,6 +33,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.firebase.ui.firestore.paging.FirestorePagingAdapter import com.firebase.ui.firestore.paging.FirestorePagingOptions +import com.firebaseui.android.demo.R import com.google.firebase.auth.FirebaseAuth import com.google.firebase.firestore.CollectionReference import com.google.firebase.firestore.FirebaseFirestore @@ -129,7 +130,8 @@ class ScoreAdapter(options: FirestorePagingOptions) : override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ScoreViewHolder(parent) override fun onBindViewHolder(holder: ScoreViewHolder, position: Int, model: ScoreItem) { - (holder.itemView as TextView).text = "${model.name} — score: ${model.score}" + val row = holder.itemView as TextView + row.text = row.context.getString(R.string.demo_score_row, model.name, model.score) } } diff --git a/app/src/main/res/drawable-hdpi/firebase_auth.png b/app/src/main/res/drawable-hdpi/firebase_auth.png deleted file mode 100644 index fecbcb6dd..000000000 Binary files a/app/src/main/res/drawable-hdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/firebase_auth.webp b/app/src/main/res/drawable-hdpi/firebase_auth.webp new file mode 100644 index 000000000..ecdf81adb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-mdpi/firebase_auth.png b/app/src/main/res/drawable-mdpi/firebase_auth.png deleted file mode 100644 index bc9af3cc0..000000000 Binary files a/app/src/main/res/drawable-mdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/firebase_auth.webp b/app/src/main/res/drawable-mdpi/firebase_auth.webp new file mode 100644 index 000000000..cb5675df3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index fde1368fc..000000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-xhdpi/firebase_auth.png b/app/src/main/res/drawable-xhdpi/firebase_auth.png deleted file mode 100644 index 8a93e39a6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/firebase_auth.webp b/app/src/main/res/drawable-xhdpi/firebase_auth.webp new file mode 100644 index 000000000..1bfda9b10 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-xxhdpi/firebase_auth.png b/app/src/main/res/drawable-xxhdpi/firebase_auth.png deleted file mode 100644 index c01b18b14..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/firebase_auth.webp b/app/src/main/res/drawable-xxhdpi/firebase_auth.webp new file mode 100644 index 000000000..600f00a5d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable-xxxhdpi/firebase_auth.png b/app/src/main/res/drawable-xxxhdpi/firebase_auth.png deleted file mode 100644 index 221da4d3a..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/firebase_auth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/firebase_auth.webp b/app/src/main/res/drawable-xxxhdpi/firebase_auth.webp new file mode 100644 index 000000000..033665959 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/firebase_auth.webp differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 1e4408cae..000000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127d3..000000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0b680c7f..3d815a631 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,10 +1,19 @@ - + FirebaseUI Demo CHANGE-HERE - - APP-ID - fbAPP-ID - CHANGE-HERE - \ No newline at end of file + + %1$s — score: %2$d + + + APP-ID + fbAPP-ID + CHANGE-HERE + diff --git a/auth/build.gradle.kts b/auth/build.gradle.kts index a7afe7801..d3758ee36 100644 --- a/auth/build.gradle.kts +++ b/auth/build.gradle.kts @@ -37,14 +37,6 @@ android { } lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - // Module specific disable += mutableSetOf( "UnusedQuantity", @@ -64,10 +56,6 @@ android { "LogConditional" ) - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - // Pre-existing debt only: 168 localization findings (CPRN-432) and 12 Compose // correctness findings (CPRN-436). Every entry is suppressed; new ones still fail. baseline = file("lint-baseline.xml") diff --git a/build.gradle.kts b/build.gradle.kts index e5e280ced..c5ff47059 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,9 @@ @file:Suppress("UnstableApiUsage") +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.LibraryExtension +import com.android.build.api.dsl.Lint + plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false @@ -31,18 +35,44 @@ allprojects { } } -// Android Lint is configured per module, so there is no repo-wide entry point by default. -// This task is that entry point, and the module list is the gate's definition: -// - :app and :e2eTest declare no lint { } block yet, so they are deliberately absent (CPRN-433). +// The shared Android Lint policy, alongside the checkstyle one above. Modules add only their +// own disables; the strictness flags live here so a module cannot quietly opt out of the gate +// the way :library did with abortOnError = false. +fun Lint.applyCommonPolicy() { + disable += setOf( + "IconExpectedSize", + "InvalidPackage", // Firestore uses GRPC which makes lint mad + "NewerVersionAvailable", "GradleDependency", // For reproducible builds + "SelectableText", "SyntheticAccessor" // We almost never care about this + ) + + checkAllWarnings = true + warningsAsErrors = true + abortOnError = true +} + +subprojects { + plugins.withId("com.android.application") { + extensions.configure { lint { applyCommonPolicy() } } + } + plugins.withId("com.android.library") { + extensions.configure { lint { applyCommonPolicy() } } + } +} + +// Android Lint has no repo-wide entry point by default. This task is that entry point, and +// the module list is the gate's definition: // - :proguard-tests disables its debug variant on CI, so it is gated on release instead. tasks.register("lintAll") { group = "verification" - description = "Runs Android Lint for every module that configures a lint { } block." + description = "Runs Android Lint for every module gated on it." dependsOn( + ":app:lintDebug", ":auth:lintDebug", ":common:lintDebug", ":database:lintDebug", + ":e2eTest:lintDebug", ":firestore:lintDebug", ":library:lintDebug", ":storage:lintDebug", diff --git a/common/build.gradle.kts b/common/build.gradle.kts index 7b8e3c497..5fcb8e613 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -23,20 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } buildTypes { named("release").configure { diff --git a/database/build.gradle.kts b/database/build.gradle.kts index 7e308b9e5..763627927 100644 --- a/database/build.gradle.kts +++ b/database/build.gradle.kts @@ -25,20 +25,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } buildTypes { named("release").configure { diff --git a/e2eTest/build.gradle.kts b/e2eTest/build.gradle.kts index e54f6003d..b7b294807 100644 --- a/e2eTest/build.gradle.kts +++ b/e2eTest/build.gradle.kts @@ -14,6 +14,7 @@ android { minSdk = Config.SdkVersions.min } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/firestore/build.gradle.kts b/firestore/build.gradle.kts index 83ad1617d..e2a6feb39 100644 --- a/firestore/build.gradle.kts +++ b/firestore/build.gradle.kts @@ -24,20 +24,6 @@ android { } } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } buildTypes { named("release").configure { diff --git a/internal/lintchecks/build.gradle.kts b/internal/lintchecks/build.gradle.kts index fc24e7762..aec59f052 100644 --- a/internal/lintchecks/build.gradle.kts +++ b/internal/lintchecks/build.gradle.kts @@ -23,21 +23,6 @@ android { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } - - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } } dependencies { diff --git a/library/build.gradle.kts b/library/build.gradle.kts index 8cad8e6b0..d709bba4a 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -23,20 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } } dependencies { diff --git a/okf-bundle/ci-workflows/android.md b/okf-bundle/ci-workflows/android.md index 92631d23e..092f220cf 100644 --- a/okf-bundle/ci-workflows/android.md +++ b/okf-bundle/ci-workflows/android.md @@ -46,7 +46,7 @@ Step 4 is **Java-only** (`include("**/*.java")`), so it inspects zero files in t Separate workflow, `pull_request` only, running `./gradlew --max-workers=2 lintAll`. -`lintAll` is registered in the root `build.gradle.kts` and gates the 8 modules that configure a `lint { }` block; `:app` and `:e2eTest` are not yet among them (CPRN-433). +`lintAll` is registered in the root `build.gradle.kts` and gates all 10 Android modules. The strictness flags and the common `disable` set live in a shared policy in that same file, applied to every module that applies the application or library plugin; a module's own `lint { }` block carries only its module-specific disables. It is a separate workflow rather than a step in `build.sh` for two reasons. Lint measured **~4-5 minutes** on this repo — the `build` job went from 3-6 min to 8-11 min when it was inline — so running it in parallel roughly halves PR feedback time at about the same total runner cost, since the extra compile the lint job pays is the one `build.sh` stops paying (`lintAnalyze` depends on `compileDebugKotlin`, and there is no remote build cache: Develocity here is configured for build scans only). And under `set -e` an inline lint failure aborted the run **before** `testDebugUnitTest`, so one new finding cost you every test result for that run. diff --git a/okf-bundle/testing/agent-command-policy.md b/okf-bundle/testing/agent-command-policy.md index 5e052d924..1f7fc5ce1 100644 --- a/okf-bundle/testing/agent-command-policy.md +++ b/okf-bundle/testing/agent-command-policy.md @@ -31,7 +31,7 @@ Single source for **which shell commands agents may run** in this repo. E2e is a | Unit tests (one module with a real `src/test` suite) | `./gradlew ::testDebugUnitTest` (e.g. `:auth:testDebugUnitTest`, `:firestore:…`, `:storage:…`) | `:common:testDebugUnitTest` / `:database:testDebugUnitTest` as “green” evidence (empty suites — [empty unit-suite trap](#empty-unit-suite-trap)); full suite when only one module changed *as a substitute for* the CI path at handoff | | Assemble one module (when no JVM unit suite) | `./gradlew ::assembleDebug` (e.g. `:database`, `:common`) | Treating empty `testDebugUnitTest` as validation | | Checkstyle (**Java only**) | `./gradlew checkstyle` | Invented ktlint/detekt entrypoints; treating a green checkstyle as style coverage for Kotlin sources ([Kotlin blind spot](#checkstyle-kotlin-blind-spot)) | -| Android Lint (all gated modules) — **not** in `build.sh`, own workflow | `./gradlew lintAll` | Bare `./gradlew lint` / `lintDebug` (pulls `:app` and `:e2eTest`, which declare no `lint { }` block yet); assuming a green `build.sh` covered lint | +| Android Lint (all gated modules) — **not** in `build.sh`, own workflow | `./gradlew lintAll` | Bare `./gradlew lint` / `lintDebug` (pulls variants `lintAll` does not gate, e.g. `:proguard-tests` debug); assuming a green `build.sh` covered lint | | Android Lint (one module) | `./gradlew ::lintDebug` (`:proguard-tests` uses `lintRelease`) | Editing Kotlin in a gated module without re-running lint | | Accept new lint debt (**needs a human decision**) | `./gradlew :auth:updateLintBaseline` | Running this to make a red build green — see [lint baseline trap](#lint-baseline-trap) | | Assemble debug | `./gradlew assembleDebug` | Module-scoped assemble as the only CI substitute at handoff | diff --git a/okf-bundle/testing/validation-checklist.md b/okf-bundle/testing/validation-checklist.md index f8764672e..0233df3c1 100644 --- a/okf-bundle/testing/validation-checklist.md +++ b/okf-bundle/testing/validation-checklist.md @@ -62,9 +62,9 @@ Instrumented `androidTest` (database/firestore) is **not** in CI or the agent al ./gradlew lintAll # Android Lint — reads Kotlin and resources ``` -`checkstyle` is scoped `include("**/*.java")` from the root `build.gradle.kts`, so on a Kotlin-only diff it inspects **zero files and exits 0**. A green checkstyle is not evidence for a change in `:auth`, `:app` or `:e2eTest` — [Kotlin blind spot](agent-command-policy.md#checkstyle-kotlin-blind-spot). +`checkstyle` is scoped `include("**/*.java")` from the root `build.gradle.kts`, so on a Kotlin-only diff it inspects **zero files and exits 0**. A green checkstyle is not evidence for a change in `:auth`, `:app` or `:e2eTest`; `lintAll` is what covers those — [Kotlin blind spot](agent-command-policy.md#checkstyle-kotlin-blind-spot). -`lintAll` runs Android Lint for the 8 modules that configure a `lint { }` block, each at `checkAllWarnings = true`, `warningsAsErrors = true` and `abortOnError = true` — so any new finding fails the build. It runs in its own workflow ([lint.yml](../ci-workflows/android.md#lint-workflow)), **not** in `build.sh`, so you must run it separately — a green `build.sh` says nothing about lint. `:app` and `:e2eTest` are not yet gated (CPRN-433). Config: each module's `lint { }` block; `library/quality/checkstyle.xml` for checkstyle. +`lintAll` runs Android Lint for all 10 Android modules at `checkAllWarnings = true`, `warningsAsErrors = true` and `abortOnError = true` — so any new finding fails the build. It runs in its own workflow ([lint.yml](../ci-workflows/android.md#lint-workflow)), **not** in `build.sh`, so you must run it separately — a green `build.sh` says nothing about lint. Config: the shared policy in the root `build.gradle.kts` sets those flags and the common `disable` set, and a module's own `lint { }` block adds only its module-specific disables; `library/quality/checkstyle.xml` for checkstyle. `auth/lint-baseline.xml` suppresses 180 pre-existing findings. **Never** run `updateLintBaseline` to clear a failure your change caused — [baseline trap](agent-command-policy.md#lint-baseline-trap). diff --git a/proguard-tests/build.gradle.kts b/proguard-tests/build.gradle.kts index 0a419b740..d76d08a70 100644 --- a/proguard-tests/build.gradle.kts +++ b/proguard-tests/build.gradle.kts @@ -46,20 +46,11 @@ android { } lint { - // Common lint options across all modules + // Module specific disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor", // We almost never care about this "MediaCapabilities", "MissingApplicationIcon" ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - } androidComponents { diff --git a/storage/build.gradle.kts b/storage/build.gradle.kts index 0e6f8af17..f36f569a2 100644 --- a/storage/build.gradle.kts +++ b/storage/build.gradle.kts @@ -23,21 +23,6 @@ android { targetCompatibility = JavaVersion.VERSION_1_8 } - lint { - // Common lint options across all modules - disable += mutableSetOf( - "IconExpectedSize", - "InvalidPackage", // Firestore uses GRPC which makes lint mad - "NewerVersionAvailable", "GradleDependency", // For reproducible builds - "SelectableText", "SyntheticAccessor" // We almost never care about this - ) - - checkAllWarnings = true - warningsAsErrors = true - abortOnError = true - - } - buildTypes { named("release").configure { isMinifyEnabled = false