Fix coroutine scope (#4820)

* Inject the session scope instead of the application scope where it's possible.

* Create AppCoroutineScope annotation to let developers explicitly choose the appropriate CoroutineScope when injecting one.
This commit is contained in:
Benoit Marty 2025-06-04 17:33:51 +02:00 committed by GitHub
parent 36c7c7ab9b
commit 5f191d9f9c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 172 additions and 72 deletions

View file

@ -0,0 +1,18 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.di.annotations
import javax.inject.Qualifier
/**
* Qualifies a [CoroutineScope] object which represents the base coroutine scope to use for the application.
*/
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Qualifier
annotation class AppCoroutineScope

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.matrix.impl
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.CacheDirectory
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.impl.analytics.UtdTracker
@ -40,6 +41,7 @@ import javax.inject.Inject
class RustMatrixClientFactory @Inject constructor(
private val baseDirectory: File,
@CacheDirectory private val cacheDirectory: File,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val coroutineDispatchers: CoroutineDispatchers,
private val sessionStore: SessionStore,

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.audio.api.AudioFocusRequester
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.mediaplayer.api.MediaPlayer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
@ -37,7 +38,8 @@ import kotlin.time.Duration.Companion.seconds
@SingleIn(RoomScope::class)
class DefaultMediaPlayer @Inject constructor(
private val player: SimplePlayer,
private val coroutineScope: CoroutineScope,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val audioFocus: AudioFocus,
) : MediaPlayer {
private val listener = object : SimplePlayer.Listener {
@ -50,7 +52,7 @@ class DefaultMediaPlayer @Inject constructor(
)
}
if (isPlaying) {
job = coroutineScope.launch { updateCurrentPosition() }
job = sessionCoroutineScope.launch { updateCurrentPosition() }
} else {
audioFocus.releaseAudioFocus()
job?.cancel()

View file

@ -423,7 +423,7 @@ class DefaultMediaPlayerTest {
audioFocus: AudioFocus = FakeAudioFocus(),
): DefaultMediaPlayer = DefaultMediaPlayer(
player = simplePlayer,
coroutineScope = backgroundScope,
sessionCoroutineScope = backgroundScope,
audioFocus = audioFocus,
)
}

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
@ -48,6 +49,7 @@ class DefaultNotificationDrawerManager @Inject constructor(
private val notificationManager: NotificationManagerCompat,
private val notificationRenderer: NotificationRenderer,
private val appNavigationStateService: AppNavigationStateService,
@AppCoroutineScope
coroutineScope: CoroutineScope,
private val matrixClientProvider: MatrixClientProvider,
private val imageLoaderHolder: ImageLoaderHolder,

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications
import android.content.Intent
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.MatrixClientProvider
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
@ -36,6 +37,7 @@ import javax.inject.Inject
private val loggerTag = LoggerTag("NotificationBroadcastReceiverHandler", LoggerTag.NotificationLoggerTag)
class NotificationBroadcastReceiverHandler @Inject constructor(
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
private val matrixClientProvider: MatrixClientProvider,
private val sessionPreferencesStore: SessionPreferencesStoreFactory,

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.notifications
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.SessionId
@ -34,6 +35,7 @@ import kotlin.time.Duration.Companion.milliseconds
@SingleIn(AppScope::class)
class NotificationResolverQueue @Inject constructor(
private val notifiableEventResolver: NotifiableEventResolver,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
) {
companion object {

View file

@ -14,6 +14,7 @@ import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.push.impl.history.PushHistoryService
import io.element.android.libraries.push.impl.history.onDiagnosticPush
@ -58,6 +59,7 @@ class DefaultPushHandler @Inject constructor(
private val notificationChannels: NotificationChannels,
private val pushHistoryService: PushHistoryService,
private val resolverQueue: NotificationResolverQueue,
@AppCoroutineScope
private val appCoroutineScope: CoroutineScope,
) : PushHandler {
init {

View file

@ -9,6 +9,7 @@ package io.element.android.libraries.push.impl.push
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.push.impl.notifications.DefaultNotificationDrawerManager
import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent
import io.element.android.libraries.push.impl.notifications.model.NotifiableRingingCallEvent
@ -23,6 +24,7 @@ interface OnNotifiableEventReceived {
@ContributesBinding(AppScope::class)
class DefaultOnNotifiableEventReceived @Inject constructor(
private val defaultNotificationDrawerManager: DefaultNotificationDrawerManager,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
) : OnNotifiableEventReceived {

View file

@ -17,6 +17,7 @@ import androidx.core.text.inSpans
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.push.impl.notifications.ActiveNotificationsProvider
import io.element.android.libraries.push.impl.notifications.NotificationDisplayer
import io.element.android.libraries.push.impl.notifications.factories.DefaultNotificationCreator
@ -36,6 +37,7 @@ interface OnRedactedEventReceived {
class DefaultOnRedactedEventReceived @Inject constructor(
private val activeNotificationsProvider: ActiveNotificationsProvider,
private val notificationDisplayer: NotificationDisplayer,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
@ApplicationContext private val context: Context,
private val stringProvider: StringProvider,

View file

@ -11,6 +11,7 @@ import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.pushproviders.api.PushHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -23,6 +24,7 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() {
@Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler
@Inject lateinit var pushParser: FirebasePushParser
@Inject lateinit var pushHandler: PushHandler
@AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope
override fun onCreate() {

View file

@ -11,6 +11,7 @@ import android.content.Context
import android.content.Intent
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.pushproviders.api.PushHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.EndpointRegistrationHandler
import io.element.android.libraries.pushproviders.unifiedpush.registration.RegistrationResult
@ -34,6 +35,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() {
@Inject lateinit var unifiedPushGatewayUrlResolver: UnifiedPushGatewayUrlResolver
@Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler
@Inject lateinit var endpointRegistrationHandler: EndpointRegistrationHandler
@AppCoroutineScope
@Inject lateinit var coroutineScope: CoroutineScope
override fun onReceive(context: Context, intent: Intent) {

View file

@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.AppCoroutineScope
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
@ -28,6 +29,7 @@ import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultSessionObserver @Inject constructor(
private val sessionStore: SessionStore,
@AppCoroutineScope
private val coroutineScope: CoroutineScope,
private val dispatchers: CoroutineDispatchers,
) : SessionObserver {

View file

@ -10,6 +10,7 @@ package io.element.android.libraries.voiceplayer.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.voiceplayer.api.VoiceMessagePresenterFactory
@ -22,7 +23,8 @@ import kotlin.time.Duration
@ContributesBinding(RoomScope::class)
class DefaultVoiceMessagePresenterFactory @Inject constructor(
private val analyticsService: AnalyticsService,
private val scope: CoroutineScope,
@SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope,
private val voiceMessagePlayerFactory: VoiceMessagePlayer.Factory,
) : VoiceMessagePresenterFactory {
override fun createVoiceMessagePresenter(
@ -41,7 +43,7 @@ class DefaultVoiceMessagePresenterFactory @Inject constructor(
return VoiceMessagePresenter(
analyticsService = analyticsService,
scope = scope,
sessionCoroutineScope = sessionCoroutineScope,
player = player,
eventId = eventId,
duration = duration,

View file

@ -31,7 +31,7 @@ import kotlin.time.Duration.Companion.milliseconds
class VoiceMessagePresenter(
private val analyticsService: AnalyticsService,
private val scope: CoroutineScope,
private val sessionCoroutineScope: CoroutineScope,
private val player: VoiceMessagePlayer,
private val eventId: EventId?,
private val duration: Duration,
@ -92,7 +92,7 @@ class VoiceMessagePresenter(
} else if (playerState.isReady) {
player.play()
} else {
scope.launch {
sessionCoroutineScope.launch {
play.runUpdatingState(
errorTransform = {
analyticsService.trackError(

View file

@ -236,7 +236,7 @@ fun TestScope.createVoiceMessagePresenter(
mediaSource: MediaSource = MediaSource(contentUri),
) = VoiceMessagePresenter(
analyticsService = analyticsService,
scope = this,
sessionCoroutineScope = this,
player = DefaultVoiceMessagePlayer(
mediaPlayer = mediaPlayer,
voiceMessageMediaRepoFactory = { _, _, _ -> voiceMessageMediaRepo },

View file

@ -15,6 +15,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.coroutine.childScope
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.voicerecorder.api.VoiceRecorder
import io.element.android.libraries.voicerecorder.api.VoiceRecorderState
import io.element.android.libraries.voicerecorder.impl.audio.Audio
@ -51,10 +52,11 @@ class DefaultVoiceRecorder @Inject constructor(
private val config: AudioConfig,
private val fileConfig: VoiceFileConfig,
private val audioLevelCalculator: AudioLevelCalculator,
appCoroutineScope: CoroutineScope,
@SessionCoroutineScope
sessionCoroutineScope: CoroutineScope,
) : VoiceRecorder {
private val voiceCoroutineScope by lazy {
appCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}")
sessionCoroutineScope.childScope(dispatchers.io, "VoiceRecorder-${UUID.randomUUID()}")
}
private var outputFile: File? = null

View file

@ -141,7 +141,7 @@ class DefaultVoiceRecorderTest {
fileConfig = fileConfig,
fileManager = FakeVoiceFileManager(fakeFileSystem, fileConfig, FILE_ID),
audioLevelCalculator = FakeAudioLevelCalculator(),
appCoroutineScope = backgroundScope,
sessionCoroutineScope = backgroundScope,
)
}