Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ demonstrate the same features and must stay in sync.
- Adhere to formatting rules defined in `.editorconfig`.
- Do not use wildcard imports (`import foo.*`); use explicit imports.
- Avoid fully qualified class names in source code; declare explicit imports at the file level instead (except to resolve naming collisions or in XML layouts).
- Target **Java 17** (`JavaLanguageVersion.of(17)`) for all project modules. Do not downgrade bytecode to Java 8 or Java 11.

## Building and testing

Expand Down Expand Up @@ -69,7 +70,10 @@ into the root `settings.gradle.kts`; build them from their own directory.

- Use Conventional Commit messages (`feat:`, `fix:`, `docs:`, ...).
release-please parses them to generate versions and CHANGELOG.md; a wrong
prefix causes a wrong release bump. Never edit CHANGELOG.md by hand.
prefix causes a wrong release bump.
- **PR Title Validation**: Ensure PR titles strictly conform to Conventional Commits (e.g., `fix: stale QuadItem removal` instead of `Fix stale QuadItem removal`). When PRs are squash-merged into `main`, GitHub uses the PR title as the default commit header; a non-conforming title prevents release-please from accurately categorizing changes in CHANGELOG.md or calculating semantic version increments.
- Never edit CHANGELOG.md or `.release-please-manifest.json` by hand.
- All pull requests are to be created as drafts (`gh pr create --draft`) until authorization is explicitly given to mark them ready for review. Always inform the user that the PR was created as a draft.
- Keep changes scoped to one sample or one feature across its language
variants; do not mix unrelated samples in one PR.
- Build and test the affected modules before declaring work done, and report actual
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.secrets.gradle.plugin) apply false
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.ksp) apply false
Expand Down
113 changes: 113 additions & 0 deletions check_api_key.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import java.util.Properties
import org.gradle.api.GradleException

/**
* Standalone API Key & Secrets Enforcement Script
*
* Checks for a valid Google Maps API key (starts with 'AIza') before executing build tasks.
* Designed to be imported on-demand into application modules.
*/

fun resolveSecretsFile(): File {
val localFile = rootProject.file("secrets.properties")
if (localFile.exists()) return localFile

// 2. Parent directory (for worktrees)
val parentFile = File(rootProject.projectDir.parentFile, "secrets.properties")
if (parentFile.exists()) {
println("Found master secrets.properties in parent directory (${parentFile.absolutePath}). Linking locally.")
try {
java.nio.file.Files.createSymbolicLink(localFile.toPath(), parentFile.toPath())
} catch (e: Exception) {
localFile.writeBytes(parentFile.readBytes())
}
return localFile
}

// 3. User Home Directory (~/.android/secrets.properties)
val homeFile = File(File(System.getProperty("user.home"), ".android"), "secrets.properties")
if (homeFile.exists()) {
println("Found master secrets.properties in ~/.android/secrets.properties. Linking locally.")
try {
java.nio.file.Files.createSymbolicLink(localFile.toPath(), homeFile.toPath())
} catch (e: Exception) {
localFile.writeBytes(homeFile.readBytes())
}
return localFile
}

return localFile // Fallback to local to trigger creation prompt
}

val secretsFile = resolveSecretsFile()
println("Resolved secrets.properties path: ${secretsFile.absolutePath}")
val isCI = System.getenv("CI")?.toBoolean() ?: false

if (!isCI) {
val requestedTasks = gradle.startParameter.taskNames

// 1. Allow Android Studio IDE sync (empty task invocation) to complete successfully
if (requestedTasks.isEmpty() && !secretsFile.exists()) {
println("⚠️ Warning: secrets.properties not found. Gradle sync will succeed, but building/running the app will fail.")
} else if (requestedTasks.isNotEmpty()) {

// 2. Identify if the current invocation builds or installs the application
val buildTaskKeywords = listOf("build", "install", "assemble", "bundle")
val isBuildTask = requestedTasks.any { task ->
buildTaskKeywords.any { keyword -> task.contains(keyword, ignoreCase = true) }
}

// 3. Allow pure verification runs (unit tests, static analysis) to proceed without keys
val testTaskKeywords = listOf("test", "report", "lint")
val isTestTask = requestedTasks.any { task ->
testTaskKeywords.any { keyword -> task.contains(keyword, ignoreCase = true) }
}

if (isBuildTask && !isTestTask) {
val defaultsFile = rootProject.file("local.defaults.properties")
val requiredKeysMessage = if (defaultsFile.exists()) {
defaultsFile.readText()
} else {
"MAPS_API_KEY=<YOUR_API_KEY>"
}

if (!secretsFile.exists()) {
throw GradleException(
"Build Blocked: 'secrets.properties' file not found.\n" +
"Please create 'secrets.properties' in the root project directory with the following content:\n\n" +
requiredKeysMessage
)
}

// 4. Validate key integrity via Properties and Regex checking
val secrets = Properties()
secretsFile.inputStream().use { secrets.load(it) }

// Check for relevant key names (e.g., MAPS_API_KEY or MAPS3D_API_KEY)
val apiKey = (secrets.getProperty("MAPS_API_KEY") ?: secrets.getProperty("MAPS3D_API_KEY") ?: "").trim()

if (apiKey.isBlank() || !apiKey.matches(Regex("^AIza[a-zA-Z0-9_-]{35}$"))) {
throw GradleException(
"Build Blocked: Invalid or missing Google Maps API key in 'secrets.properties'.\n" +
"Please provide a valid API key starting with 'AIza'."
)
}
}
}
}
14 changes: 14 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ places = "5.3.0"
playServicesLocation = "21.4.0"
playServicesMaps = "20.0.0"
secretsGradlePlugin = "2.0.1"
room = "2.8.5"

# Wear OS
wear = "1.4.0"
Expand All @@ -68,6 +69,8 @@ firebaseDatabase = "22.0.1"
# Third Party
easypermissions = "3.0.0"
volley = "1.2.1"
ktor = "3.5.2"
kotlinxSerialization = "1.11.0"

[libraries]
# Kotlin
Expand Down Expand Up @@ -110,6 +113,11 @@ places = { group = "com.google.android.libraries.places", name = "places", versi
play-services-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playServicesLocation" }
play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" }

# Room
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }

# Wear OS
wear = { group = "androidx.wear", name = "wear", version.ref = "wear" }
wearable-compile = { group = "com.google.android.wearable", name = "wearable", version.ref = "wearable" }
Expand Down Expand Up @@ -142,11 +150,17 @@ firebase-database = { module = "com.google.firebase:firebase-database", version.
# Third Party
easypermissions = { group = "pub.devrel", name = "easypermissions", version.ref = "easypermissions" }
volley = { group = "com.android.volley", name = "volley", version.ref = "volley" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
android-library = { id = "com.android.library", version.ref = "androidGradlePlugin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

Expand Down
1 change: 0 additions & 1 deletion tutorials/kotlin/MapWithMarker/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin")
}

Expand Down