Skip to content

Commit d8c615b

Browse files
committed
decoupling consumer/producer in ChangeManager
1 parent 5377e4d commit d8c615b

2 files changed

Lines changed: 103 additions & 1 deletion

File tree

AnkiDroid/src/main/java/com/ichi2/anki/observability/ChangeManager.kt

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,60 @@ import anki.collection.opChanges
4040
import anki.import_export.ImportResponse
4141
import com.ichi2.anki.common.crashreporting.CrashReportService
4242
import com.ichi2.anki.utils.ext.ifNotZero
43+
import kotlinx.coroutines.CoroutineScope
44+
import kotlinx.coroutines.Dispatchers
45+
import kotlinx.coroutines.SupervisorJob
46+
import kotlinx.coroutines.channels.BufferOverflow
47+
import kotlinx.coroutines.flow.MutableSharedFlow
48+
import kotlinx.coroutines.launch
4349
import org.jetbrains.annotations.Contract
4450
import timber.log.Timber
4551
import java.lang.ref.WeakReference
4652
import java.util.concurrent.CopyOnWriteArrayList
4753

4854
object ChangeManager {
55+
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
56+
57+
private val changeFlow =
58+
MutableSharedFlow<Pair<OpChanges, Any?>>(
59+
extraBufferCapacity = 64,
60+
onBufferOverflow = BufferOverflow.DROP_OLDEST,
61+
)
62+
63+
init {
64+
scope.launch {
65+
changeFlow.collect { (changes, handler) ->
66+
notifySubscribers(changes, handler)
67+
}
68+
}
69+
}
70+
71+
fun <T : Any> publish(
72+
changes: T,
73+
handler: Any? = null,
74+
) {
75+
val opChanges =
76+
when (changes) {
77+
is OpChanges -> changes
78+
is OpChangesWithCount -> changes.changes
79+
is OpChangesWithId -> changes.changes
80+
is OpChangesAfterUndo -> changes.changes
81+
is OpChangesOnly -> changes.changes
82+
is ImportResponse -> changes.changes
83+
else -> {
84+
Timber.e("unhandled change type: ${changes::class}")
85+
return
86+
}
87+
}
88+
89+
val success = changeFlow.tryEmit(Pair(opChanges, handler))
90+
if (!success) {
91+
scope.launch {
92+
changeFlow.emit(Pair(opChanges, handler))
93+
}
94+
}
95+
}
96+
4997
// do not make this a 'fun interface' - lambdas may immediately be GCed
5098
// due to the use of WeakReference
5199
interface Subscriber {
@@ -156,7 +204,10 @@ object ChangeManager {
156204
is OpChangesAfterUndo -> changes.changes
157205
is OpChangesOnly -> changes.changes
158206
is ImportResponse -> changes.changes
159-
else -> TODO("unhandled change type of class '${changes::class}'")
207+
else -> {
208+
Timber.e("unhandled change type: ${changes::class}")
209+
return
210+
}
160211
}
161212
notifySubscribers(opChanges, initiator)
162213
}

AnkiDroid/src/test/java/com/ichi2/anki/observability/ChangeManagerTest.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.ichi2.anki.observability
1717

1818
import anki.collection.OpChanges
1919
import com.ichi2.testutils.JvmTest
20+
import kotlinx.coroutines.test.advanceUntilIdle
2021
import org.hamcrest.MatcherAssert.assertThat
2122
import org.hamcrest.Matchers.equalTo
2223
import org.hamcrest.Matchers.greaterThan
@@ -25,6 +26,7 @@ import org.junit.Test
2526
import org.junit.runner.RunWith
2627
import org.junit.runners.Parameterized
2728
import org.robolectric.RobolectricTestRunner
29+
import org.robolectric.shadows.ShadowLooper
2830
import kotlin.reflect.KProperty1
2931
import kotlin.reflect.full.memberProperties
3032
import kotlin.reflect.javaType
@@ -107,3 +109,52 @@ class ChangeManagerTest {
107109
}
108110
}
109111
}
112+
113+
@RunWith(RobolectricTestRunner::class)
114+
class ChangeManagerPublishTest : JvmTest() {
115+
@After
116+
override fun tearDown() {
117+
super.tearDown()
118+
ChangeManager.clearSubscribers()
119+
}
120+
121+
@Test
122+
fun `publish notifies multiple subscribers asynchronously`() =
123+
runTest {
124+
ShadowLooper.idleMainLooper()
125+
126+
var firstNotified = false
127+
var secondNotified = false
128+
129+
val subscriber1 =
130+
object : ChangeManager.Subscriber {
131+
override fun opExecuted(
132+
changes: OpChanges,
133+
handler: Any?,
134+
) {
135+
firstNotified = true
136+
}
137+
}
138+
139+
val subscriber2 =
140+
object : ChangeManager.Subscriber {
141+
override fun opExecuted(
142+
changes: OpChanges,
143+
handler: Any?,
144+
) {
145+
secondNotified = true
146+
}
147+
}
148+
149+
ChangeManager.subscribe(subscriber1)
150+
ChangeManager.subscribe(subscriber2)
151+
152+
ChangeManager.publish(ChangeManager.ALL)
153+
154+
advanceUntilIdle()
155+
ShadowLooper.idleMainLooper()
156+
157+
assertThat("First subscriber should be notified", firstNotified, equalTo(true))
158+
assertThat("Second subscriber should be notified", secondNotified, equalTo(true))
159+
}
160+
}

0 commit comments

Comments
 (0)