diff --git a/.releaserc b/.releaserc
index 837a4bf4..8804b8ec 100644
--- a/.releaserc
+++ b/.releaserc
@@ -6,14 +6,12 @@ plugins:
- - "@google/semantic-release-replace-plugin"
- replacements:
- files:
- - "./Maps3DSamples/ApiDemos/kotlin-app/build.gradle.kts"
- - "./Maps3DSamples/advanced/app/build.gradle.kts"
- from: "versionName = \".*\""
- to: "versionName = \"${nextRelease.version}\""
+ - "./build.gradle.kts"
+ from: "version = \".*\""
+ to: "version = \"${nextRelease.version}\""
- - "@semantic-release/git"
- assets:
- - "Maps3DSamples/ApiDemos/kotlin-app/build.gradle.kts"
- - "Maps3DSamples/advanced/app/build.gradle.kts"
+ - "build.gradle.kts"
- - "@semantic-release/github"
- assets:
- "./advanced-kotlin-debug.apk"
diff --git a/Maps3DSamples/ApiDemos/java-app/build.gradle.kts b/Maps3DSamples/ApiDemos/java-app/build.gradle.kts
index 8eedc77d..33483e41 100644
--- a/Maps3DSamples/ApiDemos/java-app/build.gradle.kts
+++ b/Maps3DSamples/ApiDemos/java-app/build.gradle.kts
@@ -81,6 +81,7 @@ dependencies {
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.play.services.base) // "com.google.android.gms:play-services-base:18.10.0"
+ implementation(project(":library"))
implementation(project(":Maps3DSamples:ApiDemos:common"))
testImplementation(libs.junit) // "junit:junit:4.13.2"
diff --git a/Maps3DSamples/ApiDemos/kotlin-app/build.gradle.kts b/Maps3DSamples/ApiDemos/kotlin-app/build.gradle.kts
index 9ba8d840..543e7bcb 100644
--- a/Maps3DSamples/ApiDemos/kotlin-app/build.gradle.kts
+++ b/Maps3DSamples/ApiDemos/kotlin-app/build.gradle.kts
@@ -35,7 +35,7 @@ android {
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
- versionName = "1.13.0"
+ versionName = project.version.toString()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -104,6 +104,7 @@ dependencies {
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.play.services.base) // "com.google.android.gms:play-services-base:18.10.0"
+ implementation(project(":library"))
implementation(project(":Maps3DSamples:ApiDemos:common"))
}
diff --git a/Maps3DSamples/ComposeDemos/app/build.gradle.kts b/Maps3DSamples/ComposeDemos/app/build.gradle.kts
index ea7d984b..affdad70 100644
--- a/Maps3DSamples/ComposeDemos/app/build.gradle.kts
+++ b/Maps3DSamples/ComposeDemos/app/build.gradle.kts
@@ -88,6 +88,7 @@ android {
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
+ implementation(project(":library"))
implementation(project(":maps3d-compose"))
implementation(project(":Maps3DSamples:ApiDemos:common"))
diff --git a/Maps3DSamples/advanced/app/build.gradle.kts b/Maps3DSamples/advanced/app/build.gradle.kts
index 4462740b..ebad08d8 100644
--- a/Maps3DSamples/advanced/app/build.gradle.kts
+++ b/Maps3DSamples/advanced/app/build.gradle.kts
@@ -39,7 +39,7 @@ android {
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.targetSdk.get().toInt()
versionCode = 1
- versionName = "1.13.0"
+ versionName = project.version.toString()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -75,7 +75,7 @@ android {
}
dependencies {
-
+ implementation(project(":library"))
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
diff --git a/PlacesUIKit3D/build.gradle.kts b/PlacesUIKit3D/build.gradle.kts
index f58fdf31..395a721c 100644
--- a/PlacesUIKit3D/build.gradle.kts
+++ b/PlacesUIKit3D/build.gradle.kts
@@ -129,6 +129,8 @@ android {
// The `dependencies` block is where we declare all the external libraries the app needs.
// These are fetched from repositories like Maven Central and Google's Maven repository.
dependencies {
+ implementation(project(":library"))
+
// --- Core AndroidX & UI Libraries ---
// These are foundational libraries for building modern Android apps.
implementation(libs.androidx.core.ktx)
diff --git a/README.md b/README.md
index d2a6ee0d..4c7c4048 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,24 @@ under [releases](https://github.com/googlemaps/android-maps3d-samples/releases)
If you are upgrading from an older version of the SDK, please refer to the [Migration Guide](MIGRATION_GUIDE.md) for details on breaking changes and how to update your codebase.
+## Internal usage attribution ID
+
+This library calls the `addInternalUsageAttributionId` method, which helps Google understand which libraries and samples are helpful to developers and is optional. Instructions for opting out of the identifier are provided below.
+
+If you wish to disable this, you can do so by removing the initializer in your `AndroidManifest.xml` using the `tools:node="remove"` attribute:
+
+```xml
+
+
+
+```
+
## Contributing
Contributions are welcome and encouraged! If you'd like to contribute, send us a [pull request] and refer to our [code of conduct] and [contributing guide].
diff --git a/build.gradle.kts b/build.gradle.kts
index 52d07da2..1f7a1b22 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -21,6 +21,12 @@ plugins {
alias(libs.plugins.spotless) apply false
}
+allprojects {
+ // {x-release-please-start-version}
+ version = "1.13.0"
+ // {x-release-please-end}
+}
+
subprojects {
tasks.withType().configureEach {
jvmArgs("--add-opens=java.base/jdk.internal.access=ALL-UNNAMED")
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ac6ed5f7..1071abee 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -38,6 +38,9 @@ mapsUtils = "6.0.0"
androidx-core-ktx = "1.8.9"
androidxArchCoreTesting = "2.2.0"
kotlinxCoroutines = "1.8.0"
+mockk = "1.14.11"
+startup-runtime = "1.2.0"
+playServicesMaps = "20.0.0"
[libraries]
androidx-arch-core-testing = { group = "androidx.arch.core", name = "core-testing", version.ref = "androidxArchCoreTesting" }
@@ -86,6 +89,9 @@ ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx
android-maps-utils = { module = "com.google.maps.android:android-maps-utils", version.ref = "mapsUtils" }
android-maps-utils-core = { module = "com.google.maps.android:android-maps-utils-core", version.ref = "mapsUtils" }
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }
+mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
+startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "startup-runtime" }
+play-services-maps = { group = "com.google.android.gms", name = "play-services-maps", version.ref = "playServicesMaps" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
diff --git a/library/.gitignore b/library/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/library/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/library/build.gradle.kts b/library/build.gradle.kts
new file mode 100644
index 00000000..fe4bc6ed
--- /dev/null
+++ b/library/build.gradle.kts
@@ -0,0 +1,114 @@
+/*
+ * 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 org.jetbrains.kotlin.gradle.dsl.JvmTarget
+
+plugins {
+ alias(libs.plugins.android.library)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.spotless)
+}
+
+configure {
+ kotlin {
+ target("**/*.kt")
+ ktlint().editorConfigOverride(
+ mapOf(
+ "indent_size" to "4",
+ "ktlint_function_naming_ignore_when_annotated_with" to "Composable",
+ "ktlint_standard_max-line-length" to "disabled",
+ ),
+ )
+ trimTrailingWhitespace()
+ endWithNewline()
+ }
+}
+
+android {
+ namespace = "com.example.maps3dsamples.library"
+ compileSdk = libs.versions.compileSdk.get().toInt()
+
+ defaultConfig {
+ minSdk = libs.versions.minSdk.get().toInt()
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ kotlin {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_11)
+ }
+ }
+}
+
+dependencies {
+ implementation(libs.play.services.maps)
+ implementation(libs.startup.runtime)
+ testImplementation(libs.junit)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.mockk)
+ testImplementation(libs.androidx.core)
+}
+
+abstract class GenerateArtifactIdTask : DefaultTask() {
+ @get:OutputDirectory
+ abstract val outputDir: DirectoryProperty
+
+ @get:Input
+ abstract val version: Property
+
+ @TaskAction
+ fun generate() {
+ val dir = outputDir.get().asFile
+ val packageName = "com.example.maps3dsamples.library.utils.meta"
+ val packagePath = packageName.replace('.', '/')
+ val outputFile = File(dir, "$packagePath/ArtifactId.kt")
+ outputFile.parentFile.mkdirs()
+ val attributionId = "gmp_git_androidmaps3dsamples_v${version.get()}"
+ outputFile.writeText(
+ """
+ package $packageName
+
+ /**
+ * Automatically generated object containing the library's attribution ID.
+ * This is used to track library usage for analytics.
+ */
+ public object AttributionId {
+ public const val VALUE: String = "$attributionId"
+ }
+ """.trimIndent(),
+ )
+ }
+}
+
+val generateArtifactIdFile = tasks.register("generateArtifactIdFile") {
+ outputDir.set(layout.buildDirectory.dir("generated/source/artifactId"))
+ version.set(project.version.toString())
+}
+
+androidComponents {
+ onVariants { variant ->
+ variant.sources.java?.addGeneratedSourceDirectory(
+ generateArtifactIdFile,
+ GenerateArtifactIdTask::outputDir,
+ )
+ }
+}
diff --git a/library/consumer-rules.pro b/library/consumer-rules.pro
new file mode 100644
index 00000000..3add6774
--- /dev/null
+++ b/library/consumer-rules.pro
@@ -0,0 +1 @@
+# Consumer ProGuard rules for attribution ID initializer
diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..150f843f
--- /dev/null
+++ b/library/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/library/src/main/java/com/example/maps3dsamples/library/utils/attribution/AttributionIdInitializer.kt b/library/src/main/java/com/example/maps3dsamples/library/utils/attribution/AttributionIdInitializer.kt
new file mode 100644
index 00000000..b31cdfe1
--- /dev/null
+++ b/library/src/main/java/com/example/maps3dsamples/library/utils/attribution/AttributionIdInitializer.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package com.example.maps3dsamples.library.utils.attribution
+
+import android.content.Context
+import androidx.annotation.Keep
+import androidx.startup.Initializer
+import com.example.maps3dsamples.library.utils.meta.AttributionId
+import com.google.android.gms.maps.MapsApiSettings
+
+/**
+ * Adds a usage attribution ID to the initializer, which helps Google understand which libraries
+ * and samples are helpful to developers, such as usage of this library.
+ * To opt out of sending the usage attribution ID, please remove this initializer from your manifest.
+ */
+@Keep
+internal class AttributionIdInitializer : Initializer {
+ override fun create(context: Context) {
+ MapsApiSettings.addInternalUsageAttributionId(
+ // context =
+ context,
+ // internalUsageAttributionId =
+ AttributionId.VALUE,
+ )
+ }
+
+ override fun dependencies(): List>> = emptyList()
+}
diff --git a/library/src/test/java/com/example/maps3dsamples/library/utils/attribution/AttributionIdInitializerTest.kt b/library/src/test/java/com/example/maps3dsamples/library/utils/attribution/AttributionIdInitializerTest.kt
new file mode 100644
index 00000000..0c4f75b3
--- /dev/null
+++ b/library/src/test/java/com/example/maps3dsamples/library/utils/attribution/AttributionIdInitializerTest.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package com.example.maps3dsamples.library.utils.attribution
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import com.example.maps3dsamples.library.utils.meta.AttributionId
+import com.google.android.gms.maps.MapsApiSettings
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockkStatic
+import io.mockk.runs
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class AttributionIdInitializerTest {
+ @Before
+ fun setUp() {
+ mockkStatic(MapsApiSettings::class)
+ every { MapsApiSettings.addInternalUsageAttributionId(any(), any()) } just runs
+ }
+
+ @After
+ fun tearDown() {
+ unmockkStatic(MapsApiSettings::class)
+ }
+
+ @Test
+ fun `create adds internal usage attribution id`() {
+ val context = ApplicationProvider.getApplicationContext()
+ val initializer = AttributionIdInitializer()
+
+ initializer.create(context)
+
+ verify {
+ MapsApiSettings.addInternalUsageAttributionId(
+ context,
+ AttributionId.VALUE,
+ )
+ }
+ }
+}
diff --git a/maps3d-compose-demo/build.gradle.kts b/maps3d-compose-demo/build.gradle.kts
index f450ae7d..11bd32d7 100644
--- a/maps3d-compose-demo/build.gradle.kts
+++ b/maps3d-compose-demo/build.gradle.kts
@@ -84,6 +84,7 @@ android {
}
dependencies {
+ implementation(project(":library"))
implementation(project(":maps3d-compose"))
implementation(libs.androidx.core.ktx)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 494cd19c..a03595f8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -69,3 +69,6 @@ include(":maps3d-compose-demo")
// Visual Testing
include(":visual-testing")
+
+// Attribution Library
+include(":library")
diff --git a/snippets/java-app/build.gradle.kts b/snippets/java-app/build.gradle.kts
index eecb7bb0..7c620f0f 100644
--- a/snippets/java-app/build.gradle.kts
+++ b/snippets/java-app/build.gradle.kts
@@ -69,6 +69,7 @@ android {
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
+ implementation(project(":library"))
implementation(project(":snippets:common"))
implementation(libs.androidx.core.ktx)
diff --git a/snippets/kotlin-app/build.gradle.kts b/snippets/kotlin-app/build.gradle.kts
index cf1fd925..fc19f1f2 100644
--- a/snippets/kotlin-app/build.gradle.kts
+++ b/snippets/kotlin-app/build.gradle.kts
@@ -71,6 +71,7 @@ android {
dependencies {
coreLibraryDesugaring(libs.desugar.jdk.libs)
+ implementation(project(":library"))
implementation(project(":snippets:common"))
implementation(libs.androidx.core.ktx)