Skip to content

Commit 0653f41

Browse files
authored
fix: ensure start method runs on main thread to prevent crashes (#345)
1 parent 7649814 commit 0653f41

File tree

2 files changed

+42
-12
lines changed

2 files changed

+42
-12
lines changed

android/src/main/java/com/amplitude/android/internal/gestures/WindowCallbackManager.kt

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package com.amplitude.android.internal.gestures
33
import android.app.Activity
44
import android.content.Context
55
import android.content.ContextWrapper
6+
import android.os.Handler
7+
import android.os.Looper
68
import android.view.View
79
import android.view.Window
810
import androidx.annotation.VisibleForTesting
@@ -33,27 +35,37 @@ internal class WindowCallbackManager(
3335
private val wrappedWindows = mutableMapOf<Window, Window.Callback?>()
3436
private var started = false
3537

38+
private val mainHandler = Handler(Looper.getMainLooper())
39+
3640
private val rootViewsChangedListener =
3741
OnRootViewsChangedListener { view, added ->
38-
if (added) {
39-
onRootViewAdded(view)
40-
} else {
41-
onRootViewRemoved(view)
42+
// ensure all window operations happen on main thread since AutocaptureWindowCallback
43+
// creates GestureDetector which requires main thread.
44+
mainHandler.post {
45+
if (added) {
46+
onRootViewAdded(view)
47+
} else {
48+
onRootViewRemoved(view)
49+
}
4250
}
4351
}
4452

4553
fun start() {
46-
started = true
47-
Curtains.onRootViewsChangedListeners += rootViewsChangedListener
48-
// Wrap any existing windows
49-
Curtains.rootViews.forEach(::onRootViewAdded)
54+
mainHandler.post {
55+
started = true
56+
Curtains.onRootViewsChangedListeners += rootViewsChangedListener
57+
// Wrap any existing windows
58+
Curtains.rootViews.forEach(::onRootViewAdded)
59+
}
5060
}
5161

5262
fun stop() {
53-
started = false
54-
Curtains.onRootViewsChangedListeners -= rootViewsChangedListener
55-
// Unwrap all windows
56-
wrappedWindows.keys.toList().forEach(::unwrapWindow)
63+
mainHandler.post {
64+
started = false
65+
Curtains.onRootViewsChangedListeners -= rootViewsChangedListener
66+
// Unwrap all windows
67+
wrappedWindows.keys.toList().forEach(::unwrapWindow)
68+
}
5769
}
5870

5971
private fun onRootViewAdded(view: View) {

android/src/test/kotlin/com/amplitude/android/internal/gestures/WindowCallbackManagerTest.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,29 @@ package com.amplitude.android.internal.gestures
33
import android.app.Activity
44
import android.content.Context
55
import android.content.ContextWrapper
6+
import android.os.Looper
67
import android.view.View
78
import android.view.Window
9+
import androidx.test.core.app.ApplicationProvider
810
import com.amplitude.android.AutocaptureState
911
import com.amplitude.android.internal.TrackFunction
1012
import com.amplitude.common.Logger
1113
import io.mockk.every
1214
import io.mockk.mockk
1315
import io.mockk.verify
1416
import org.junit.Test
17+
import org.junit.runner.RunWith
18+
import org.robolectric.RobolectricTestRunner
19+
import org.robolectric.Shadows.shadowOf
20+
import org.robolectric.annotation.Config
1521

22+
@RunWith(RobolectricTestRunner::class)
23+
@Config(manifest = Config.NONE)
1624
class WindowCallbackManagerTest {
1725
private val track = mockk<TrackFunction>(relaxed = true)
1826
private val logger = mockk<Logger>(relaxed = true)
1927
private val autocaptureState = AutocaptureState(interactions = emptyList())
28+
private val appContext: Context get() = ApplicationProvider.getApplicationContext()
2029

2130
@Test
2231
fun `wraps window callback for activity window`() {
@@ -27,6 +36,7 @@ class WindowCallbackManagerTest {
2736

2837
every { window.context } returns activity
2938
every { window.callback } returns originalCallback
39+
every { decorView.context } returns appContext
3040

3141
val sut =
3242
WindowCallbackManager(
@@ -37,6 +47,7 @@ class WindowCallbackManagerTest {
3747
)
3848

3949
sut.start()
50+
shadowOf(Looper.getMainLooper()).idle()
4051
sut.wrapWindowForTesting(window, decorView)
4152

4253
verify { window.callback = any<AutocaptureWindowCallback>() }
@@ -59,6 +70,7 @@ class WindowCallbackManagerTest {
5970
)
6071

6172
sut.start()
73+
shadowOf(Looper.getMainLooper()).idle()
6274
sut.wrapWindowForTesting(window, decorView)
6375

6476
verify(exactly = 0) { window.callback = any<AutocaptureWindowCallback>() }
@@ -77,6 +89,7 @@ class WindowCallbackManagerTest {
7789
every { wrapper.baseContext } returns activity
7890
every { window.context } returns wrapper
7991
every { window.callback } returns originalCallback
92+
every { decorView.context } returns appContext
8093

8194
val sut =
8295
WindowCallbackManager(
@@ -87,6 +100,7 @@ class WindowCallbackManagerTest {
87100
)
88101

89102
sut.start()
103+
shadowOf(Looper.getMainLooper()).idle()
90104
sut.wrapWindowForTesting(window, decorView)
91105

92106
verify { window.callback = any<AutocaptureWindowCallback>() }
@@ -101,6 +115,7 @@ class WindowCallbackManagerTest {
101115

102116
every { window.context } returns activity
103117
every { window.callback } returns originalCallback
118+
every { decorView.context } returns appContext
104119

105120
val sut =
106121
WindowCallbackManager(
@@ -111,6 +126,7 @@ class WindowCallbackManagerTest {
111126
)
112127

113128
sut.start()
129+
shadowOf(Looper.getMainLooper()).idle()
114130
sut.wrapWindowForTesting(window, decorView)
115131
sut.wrapWindowForTesting(window, decorView)
116132

@@ -126,6 +142,7 @@ class WindowCallbackManagerTest {
126142

127143
every { window.context } returns activity
128144
every { window.callback } returns originalCallback
145+
every { decorView.context } returns appContext
129146

130147
val sut =
131148
WindowCallbackManager(
@@ -136,6 +153,7 @@ class WindowCallbackManagerTest {
136153
)
137154

138155
sut.start()
156+
shadowOf(Looper.getMainLooper()).idle()
139157

140158
// Wrap
141159
sut.wrapWindowForTesting(window, decorView)

0 commit comments

Comments
 (0)