Skip to content

Commit 7126076

Browse files
committed
fix: Address IllegalStateException on main thread during map initialization
Fixes a crash where `IllegalStateException: Method addObserver must be called on the main thread` was thrown during Google Maps initialization on certain Android devices after upgrading the library. This addresses the issue reported in #789.
1 parent 3c6ced2 commit 7126076

File tree

5 files changed

+64
-8
lines changed

5 files changed

+64
-8
lines changed

maps-app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<uses-permission android:name="android.permission.INTERNET" />
2121

2222
<application
23+
android:name=".MapsComposeApplication"
2324
android:allowBackup="true"
2425
android:icon="@mipmap/ic_launcher"
2526
android:label="@string/app_name"

maps-app/src/main/java/com/google/maps/android/compose/LocationTrackingActivity.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
1615
package com.google.maps.android.compose
1716

1817
import android.location.Location
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.maps.android.compose
16+
17+
import android.app.Application
18+
import com.google.maps.android.compose.internal.DefaultGoogleMapsInitializer
19+
import kotlinx.coroutines.DelicateCoroutinesApi
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.GlobalScope
22+
import kotlinx.coroutines.launch
23+
24+
class MapsComposeApplication : Application() {
25+
@OptIn(DelicateCoroutinesApi::class)
26+
override fun onCreate() {
27+
super.onCreate()
28+
// Note: that this is not a singleton, but it should still effectively load the SDK once
29+
30+
GlobalScope.launch(Dispatchers.Main) {
31+
DefaultGoogleMapsInitializer().initialize(
32+
context = this@MapsComposeApplication,
33+
forceInitialization = false
34+
)
35+
}
36+
}
37+
38+
}

maps-app/src/test/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import io.mockk.every
2323
import io.mockk.mockk
2424
import io.mockk.mockkStatic
2525
import io.mockk.verify
26+
import kotlinx.coroutines.Dispatchers
2627
import kotlinx.coroutines.ExperimentalCoroutinesApi
2728
import kotlinx.coroutines.launch
2829
import kotlinx.coroutines.test.TestDispatcher
2930
import kotlinx.coroutines.test.UnconfinedTestDispatcher
3031
import kotlinx.coroutines.test.runTest
32+
import kotlinx.coroutines.test.setMain
3133
import org.junit.Assert.assertEquals
3234
import org.junit.Before
3335
import org.junit.Test
@@ -51,6 +53,7 @@ class GoogleMapsInitializerTest {
5153
every { MapsApiSettings.addInternalUsageAttributionId(any(), any()) } returns Unit
5254

5355
testDispatcher = UnconfinedTestDispatcher()
56+
Dispatchers.setMain(testDispatcher)
5457
googleMapsInitializer = DefaultGoogleMapsInitializer(testDispatcher)
5558
}
5659

maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.maps.android.compose.internal
1616

1717
import android.content.Context
18+
import android.os.StrictMode
1819
import androidx.compose.runtime.ProvidableCompositionLocal
1920
import androidx.compose.runtime.State
2021
import androidx.compose.runtime.compositionLocalOf
@@ -25,6 +26,7 @@ import com.google.android.gms.maps.MapsApiSettings
2526
import com.google.maps.android.compose.meta.AttributionId
2627
import kotlinx.coroutines.CoroutineDispatcher
2728
import kotlinx.coroutines.Dispatchers
29+
import kotlinx.coroutines.launch
2830
import kotlinx.coroutines.sync.Mutex
2931
import kotlinx.coroutines.sync.withLock
3032
import kotlinx.coroutines.withContext
@@ -115,7 +117,8 @@ public interface GoogleMapsInitializer {
115117
* @param ioDispatcher The dispatcher to use for IO operations.
116118
*/
117119
public class DefaultGoogleMapsInitializer(
118-
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
120+
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
121+
private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
119122
) : GoogleMapsInitializer {
120123
private val _state = mutableStateOf(InitializationState.UNINITIALIZED)
121124
override val state: State<InitializationState> = _state
@@ -142,12 +145,24 @@ public class DefaultGoogleMapsInitializer(
142145
_state.value = InitializationState.INITIALIZING
143146
}
144147

145-
withContext(ioDispatcher) {
146-
if (MapsInitializer.initialize(context) == ConnectionResult.SUCCESS) {
147-
MapsApiSettings.addInternalUsageAttributionId(context, attributionId)
148-
_state.value = InitializationState.SUCCESS
149-
} else {
150-
_state.value = InitializationState.FAILURE
148+
withContext(mainDispatcher) {
149+
val scope = this
150+
151+
val policy = StrictMode.getThreadPolicy()
152+
try {
153+
StrictMode.allowThreadDiskReads()
154+
val result = MapsInitializer.initialize(context, null) {
155+
scope.launch(ioDispatcher) {
156+
MapsApiSettings.addInternalUsageAttributionId(context, attributionId)
157+
_state.value = InitializationState.SUCCESS
158+
}
159+
}
160+
161+
if (result != ConnectionResult.SUCCESS) {
162+
_state.value = InitializationState.FAILURE
163+
}
164+
} finally {
165+
StrictMode.setThreadPolicy(policy)
151166
}
152167
}
153168
} catch (e: com.google.android.gms.common.GooglePlayServicesMissingManifestValueException) {

0 commit comments

Comments
 (0)