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
11 changes: 11 additions & 0 deletions maps-app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,15 @@ android {
imageDifferenceThreshold = 0.035f // 3.5%
}
}

packaging {
resources {
pickFirsts += listOf(
"META-INF/LICENSE.md",
"META-INF/LICENSE-notice.md"
)
}
}
}

dependencies {
Expand Down Expand Up @@ -90,6 +99,8 @@ dependencies {
androidTestImplementation(libs.androidx.test.compose.ui)
androidTestImplementation(libs.kotlinx.coroutines.test)
androidTestImplementation(libs.truth)
androidTestImplementation(libs.mockk.android)
//androidTestImplementation(kotlin("test"))

testImplementation(libs.test.junit)
testImplementation(libs.robolectric)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import kotlin.time.Duration.Companion.milliseconds
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GooglePlayServicesMissingManifestValueException
import com.google.android.gms.maps.MapsInitializer
import com.google.android.gms.maps.MapsApiSettings
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockkStatic
import io.mockk.Runs
import io.mockk.just

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
Expand Down Expand Up @@ -119,4 +128,108 @@ class GoogleMapsInitializerTest {

assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.UNINITIALIZED)
}

@Test
fun testInitializeSuccessState() = runTest {
// Arrange
mockkStatic(MapsInitializer::class)
assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.UNINITIALIZED)

coEvery { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS

val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
// Act
// Direct call pattern matching original successful test structure
googleMapsInitializer.initialize(context)

// Assert
assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
coVerify(exactly = 1) { MapsInitializer.initialize(
eq(context),
any(),
any(),
)}
}

@Test
fun testInitializeConcurrentCallsOnlyRunOnce() = runTest {
mockkStatic(MapsInitializer::class)
coEvery { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS

val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
val job1 = launch { googleMapsInitializer.initialize(context) }
val job2 = launch { googleMapsInitializer.initialize(context) }

job1.join()
job2.join()

// Assert: The actual initialization method should only have been called once
coVerify(exactly = 1) { MapsInitializer.initialize(
eq(context),
any(),
any(),
)}
assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
}

@Test
fun testInitializeUnrecoverableFailureSetsFailureState() = runTest {
// Arrange
mockkStatic(MapsInitializer::class)
val error = GooglePlayServicesMissingManifestValueException()

val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
var caughtException: Throwable? = null

coEvery {
MapsInitializer.initialize(
eq(context),
isNull(),
any()
)
} throws error

// Act
val job = launch {
try {
googleMapsInitializer.initialize(context)
} catch (e: GooglePlayServicesMissingManifestValueException) {
caughtException = e
}
}
job.join()

// Assert: The exception was caught, and the state became FAILURE
assertThat(caughtException).isInstanceOf(GooglePlayServicesMissingManifestValueException::class.java)
assertThat(caughtException).isEqualTo(error)

// 2. Assert the state was set to FAILURE
assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.FAILURE)
}

@Test
fun testInitializeSuccessAlsoSetsAttributionId() = runTest {
// Arrange: Mock MapsApiSettings locally
mockkStatic(MapsInitializer::class, MapsApiSettings::class)

coEvery { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS
coEvery { MapsApiSettings.addInternalUsageAttributionId(any(), any()) } just Runs

val context: Context = InstrumentationRegistry.getInstrumentation().targetContext

// Act
// Direct call pattern matching original successful test structure
googleMapsInitializer.initialize(context)

// Assert: Verify both the primary initialization and the attribution call occurred
coVerify(exactly = 1) {
MapsInitializer.initialize(
eq(context),
any(),
any(),
)
}
coVerify(exactly = 1) { MapsApiSettings.addInternalUsageAttributionId(any(), any()) }
assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
}
}
172 changes: 86 additions & 86 deletions maps-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
Copyright 2023 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -17,95 +16,96 @@

<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidMapsCompose" >
<application
android:name=".MapsComposeApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidMapsCompose">

<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!--
All activities in this sample are exported. This is for demonstration
purposes, to make it easy to launch each sample activity directly.
<!--
All activities in this sample are exported. This is for demonstration
purposes, to make it easy to launch each sample activity directly.

In a real-world application, you should carefully consider which activities
to export. In most cases, only the main launcher activity should be exported.
Exporting an activity means that any other app on the device can launch it.
-->

<activity
android:name=".BasicMapActivity"
android:exported="true" />
<activity
android:name=".markerexamples.AdvancedMarkersActivity"
android:exported="true"/>
<activity
android:name=".MapInColumnActivity"
android:exported="true"/>
<activity
android:name=".MapsInLazyColumnActivity"
android:exported="true"/>
<activity
android:name=".markerexamples.MarkerClusteringActivity"
android:exported="true"/>
<activity
android:name=".LocationTrackingActivity"
android:exported="true"/>
<activity
android:name=".ScaleBarActivity"
android:exported="true"/>
<activity
android:name=".StreetViewActivity"
android:exported="true"/>
<activity
android:name=".CustomControlsActivity"
android:exported="true"/>
<activity
android:name=".AccessibilityActivity"
android:exported="true"/>
<activity
android:name=".RecompositionActivity"
android:exported="true"/>
<activity
android:name=".FragmentDemoActivity"
android:exported="true"/>
<activity
android:name=".markerexamples.markerdragevents.MarkerDragEventsActivity"
android:exported="true"/>
<activity
android:name=".markerexamples.markerscollection.MarkersCollectionActivity"
android:exported="true"/>
<activity
android:name=".markerexamples.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity"
android:exported="true"/>
<activity
android:name=".markerexamples.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity"
android:exported="true"/>
<activity
android:name=".markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity"
android:exported="true"/>
<activity
android:name=".TileOverlayActivity"
android:exported="true"/>
In a real-world application, you should carefully consider which activities
to export. In most cases, only the main launcher activity should be exported.
Exporting an activity means that any other app on the device can launch it.
-->

<!-- Used by createComponentActivity() for unit testing -->
<activity android:name="androidx.activity.ComponentActivity" />
<activity
android:name=".BasicMapActivity"
android:exported="true" />
<activity
android:name=".markerexamples.AdvancedMarkersActivity"
android:exported="true" />
<activity
android:name=".MapInColumnActivity"
android:exported="true" />
<activity
android:name=".MapsInLazyColumnActivity"
android:exported="true" />
<activity
android:name=".markerexamples.MarkerClusteringActivity"
android:exported="true" />
<activity
android:name=".LocationTrackingActivity"
android:exported="true" />
<activity
android:name=".ScaleBarActivity"
android:exported="true" />
<activity
android:name=".StreetViewActivity"
android:exported="true" />
<activity
android:name=".CustomControlsActivity"
android:exported="true" />
<activity
android:name=".AccessibilityActivity"
android:exported="true" />
<activity
android:name=".RecompositionActivity"
android:exported="true" />
<activity
android:name=".FragmentDemoActivity"
android:exported="true" />
<activity
android:name=".markerexamples.markerdragevents.MarkerDragEventsActivity"
android:exported="true" />
<activity
android:name=".markerexamples.markerscollection.MarkersCollectionActivity"
android:exported="true" />
<activity
android:name=".markerexamples.syncingdraggablemarkerwithdatamodel.SyncingDraggableMarkerWithDataModelActivity"
android:exported="true" />
<activity
android:name=".markerexamples.updatingnodragmarkerwithdatamodel.UpdatingNoDragMarkerWithDataModelActivity"
android:exported="true" />
<activity
android:name=".markerexamples.draggablemarkerscollectionwithpolygon.DraggableMarkersCollectionWithPolygonActivity"
android:exported="true" />
<activity
android:name=".TileOverlayActivity"
android:exported="true" />

</application>
<!-- Used by createComponentActivity() for unit testing -->
<activity android:name="androidx.activity.ComponentActivity" />

</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.


package com.google.maps.android.compose

import android.location.Location
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2025 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.google.maps.android.compose

import android.app.Application
import com.google.maps.android.compose.internal.DefaultGoogleMapsInitializer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch

class MapsComposeApplication : Application() {

private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

override fun onCreate() {
super.onCreate()
// The DefaultGoogleMapsInitializer is not a singleton, but the Maps SDK is initialized just once.

applicationScope.launch {
DefaultGoogleMapsInitializer().initialize(
context = this@MapsComposeApplication,
forceInitialization = false
)
}
}

}
Loading
Loading