Merge pull request #7901 from element-hq/feature/bma/mainThreadBlocking

Avoid blocking the main thread in several places
This commit is contained in:
Benoit Marty
2026-10-14 16:39:14 +02:00
committed by GitHub
13 changed files with 117 additions and 74 deletions
@@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.TimelineController
import io.element.android.features.messages.impl.utils.TextPillificationHelper import io.element.android.features.messages.impl.utils.TextPillificationHelper
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
@@ -110,6 +111,7 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes import io.element.android.libraries.core.mimetype.MimeTypes.Any as AnyMimeTypes
@@ -145,6 +147,7 @@ class MessageComposerPresenter(
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val contentScannerService: ContentScannerService, private val contentScannerService: ContentScannerService,
private val contentValidationCache: EventContentValidationCache, private val contentValidationCache: EventContentValidationCache,
private val dispatchers: CoroutineDispatchers,
) : Presenter<MessageComposerState> { ) : Presenter<MessageComposerState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@@ -190,22 +193,22 @@ class MessageComposerPresenter(
.collectAsState(initial = false) .collectAsState(initial = false)
val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType -> val galleryMediaPicker = mediaPickerProvider.registerGalleryPicker { uri, mimeType ->
handlePickedMedia(uri, mimeType) localCoroutineScope.handlePickedMedia(uri, mimeType)
} }
val galleryMultiMediaPicker = mediaPickerProvider.registerGalleryMultiPicker { uris -> val galleryMultiMediaPicker = mediaPickerProvider.registerGalleryMultiPicker { uris ->
handlePickedMediaList(uris) localCoroutineScope.handlePickedMediaList(uris)
} }
val filesPicker = mediaPickerProvider.registerFileMultiPicker(AnyMimeTypes) { uris -> val filesPicker = mediaPickerProvider.registerFileMultiPicker(AnyMimeTypes) { uris ->
handlePickedMediaList(uris, sendAsFile = true) localCoroutineScope.handlePickedMediaList(uris, sendAsFile = true)
} }
val fileSinglePicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType -> val fileSinglePicker = mediaPickerProvider.registerFilePicker(AnyMimeTypes) { uri, mimeType ->
handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true) localCoroutineScope.handlePickedMedia(uri, mimeType ?: MimeTypes.OctetStream, sendAsFile = true)
} }
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri -> val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker { uri ->
handlePickedMedia(uri, MimeTypes.Jpeg) localCoroutineScope.handlePickedMedia(uri, MimeTypes.Jpeg)
} }
val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri -> val cameraVideoPicker = mediaPickerProvider.registerCameraVideoPicker { uri ->
handlePickedMedia(uri, MimeTypes.Mp4) localCoroutineScope.handlePickedMedia(uri, MimeTypes.Mp4)
} }
val isFullScreen = rememberSaveable { val isFullScreen = rememberSaveable {
mutableStateOf(false) mutableStateOf(false)
@@ -283,17 +286,16 @@ class MessageComposerPresenter(
} }
is MessageComposerEvent.SendUri -> { is MessageComposerEvent.SendUri -> {
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
sessionCoroutineScope.sendAttachment( sessionCoroutineScope.launch {
attachment = Attachment.Media( val localMedia = createLocalMedia(
localMedia = localMediaFactory.createFromUri( uri = event.uri,
uri = event.uri, mimeType = null,
mimeType = null, )
name = null, sendAttachment(
formattedFileSize = null attachment = Attachment.Media(localMedia),
), inReplyToEventId = inReplyToEventId,
), )
inReplyToEventId = inReplyToEventId, }
)
resetComposerModeAfterAttaching() resetComposerModeAfterAttaching()
} }
@@ -637,17 +639,28 @@ class MessageComposerPresenter(
} }
} }
private fun handlePickedMedia( // Creating the LocalMedia queries the ContentResolver and can decode the media, so do it off the main thread
uri: Uri?, private suspend fun createLocalMedia(
mimeType: String? = null, uri: Uri,
sendAsFile: Boolean = false, mimeType: String?,
) { ) = withContext(dispatchers.io) {
uri ?: return localMediaFactory.createFromUri(
val localMedia = localMediaFactory.createFromUri(
uri = uri, uri = uri,
mimeType = mimeType, mimeType = mimeType,
name = null, name = null,
formattedFileSize = null formattedFileSize = null,
)
}
private fun CoroutineScope.handlePickedMedia(
uri: Uri?,
mimeType: String? = null,
sendAsFile: Boolean = false,
) = launch {
uri ?: return@launch
val localMedia = createLocalMedia(
uri = uri,
mimeType = mimeType,
) )
val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile) val mediaAttachment = Attachment.Media(localMedia, sendAsFile = sendAsFile)
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
@@ -656,7 +669,7 @@ class MessageComposerPresenter(
resetComposerModeAfterAttaching() resetComposerModeAfterAttaching()
} }
private fun handlePickedMediaList( private fun CoroutineScope.handlePickedMediaList(
uris: List<Uri>, uris: List<Uri>,
sendAsFile: Boolean = false, sendAsFile: Boolean = false,
) { ) {
@@ -665,19 +678,19 @@ class MessageComposerPresenter(
handlePickedMedia(uris.first(), sendAsFile = sendAsFile) handlePickedMedia(uris.first(), sendAsFile = sendAsFile)
return return
} }
val attachments = uris.map { uri -> launch {
val localMedia = localMediaFactory.createFromUri( val attachments = uris.map { uri ->
uri = uri, val localMedia = createLocalMedia(
mimeType = null, uri = uri,
name = null, mimeType = null,
formattedFileSize = null, )
) Attachment.Media(localMedia, sendAsFile = sendAsFile)
Attachment.Media(localMedia, sendAsFile = sendAsFile) }.toImmutableList()
}.toImmutableList() val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId
val inReplyToEventId = (messageComposerContext.composerMode as? MessageComposerMode.Reply)?.eventId navigator.navigateToPreviewAttachments(attachments, inReplyToEventId)
navigator.navigateToPreviewAttachments(attachments, inReplyToEventId)
resetComposerModeAfterAttaching() resetComposerModeAfterAttaching()
}
} }
private fun resetComposerModeAfterAttaching() { private fun resetComposerModeAfterAttaching() {
@@ -324,6 +324,7 @@ class MessageComposerPresenterSlashCommandTest {
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
contentScannerService = { _, _ -> }, contentScannerService = { _, _ -> },
contentValidationCache = InMemoryEventContentValidationCache(), contentValidationCache = InMemoryEventContentValidationCache(),
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
).apply { ).apply {
isTesting = true isTesting = true
showTextFormatting = isRichTextEditorEnabled showTextFormatting = isRichTextEditorEnabled
@@ -1723,6 +1723,7 @@ class MessageComposerPresenterTest : RobolectricTest() {
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
contentScannerService = { _, _ -> }, contentScannerService = { _, _ -> },
contentValidationCache = InMemoryEventContentValidationCache(), contentValidationCache = InMemoryEventContentValidationCache(),
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
).apply { ).apply {
isTesting = true isTesting = true
showTextFormatting = isRichTextEditorEnabled showTextFormatting = isRichTextEditorEnabled
@@ -26,6 +26,7 @@ import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runUpdatingStateNoSuccess import io.element.android.libraries.architecture.runUpdatingStateNoSuccess
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
@@ -57,6 +58,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
@@ -76,6 +78,7 @@ class NotificationSettingsPresenter(
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val dispatchers: CoroutineDispatchers,
) : Presenter<NotificationSettingsState> { ) : Presenter<NotificationSettingsState> {
// Serializes the pick → copy → persist → recreate pipeline per slot. The copier already locks // Serializes the pick → copy → persist → recreate pipeline per slot. The copier already locks
// its `<slot>.tmp` file, but the persist+recreate window outside the copier is unprotected; // its `<slot>.tmp` file, but the persist+recreate window outside the copier is unprotected;
@@ -111,16 +114,19 @@ class NotificationSettingsPresenter(
} }
// List of PushProvider -> Distributor // List of PushProvider -> Distributor
val distributors = remember { // Getting the distributors can query the PackageManager and the Play Services, so do it off the main thread
pushService.getAvailablePushProviders() val distributors by produceState(emptyList<Pair<PushProvider, Distributor>>()) {
.flatMap { pushProvider -> value = withContext(dispatchers.io) {
pushProvider.getDistributors().map { distributor -> pushService.getAvailablePushProviders()
pushProvider to distributor .flatMap { pushProvider ->
pushProvider.getDistributors().map { distributor ->
pushProvider to distributor
}
} }
} }
} }
// List of Distributors // List of Distributors
val availableDistributors = remember { val availableDistributors = remember(distributors) {
distributors.map { it.second }.toImmutableList() distributors.map { it.second }.toImmutableList()
} }
@@ -40,6 +40,7 @@ import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@@ -1176,6 +1177,7 @@ class NotificationSettingsPresenterTest {
stringProvider = FakeStringProvider(), stringProvider = FakeStringProvider(),
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
dispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
) )
} }
} }
@@ -118,6 +118,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
@@ -320,7 +321,7 @@ class RustMatrixClient(
override fun onUpdate(profile: UserProfile) { override fun onUpdate(profile: UserProfile) {
val matrixUser = profile.map() val matrixUser = profile.map()
_userProfile.tryEmit(matrixUser) _userProfile.tryEmit(matrixUser)
sessionCoroutineScope.launch { sessionCoroutineScope.launch(sessionDispatcher) {
storeUserProfile(matrixUser) storeUserProfile(matrixUser)
} }
} }
@@ -1013,6 +1014,8 @@ class RustMatrixClient(
searchBackfillService.cancelPeriodicSearchBackfill() searchBackfillService.cancelPeriodicSearchBackfill()
} }
} }
// Checking the pending work is a blocking call, do not do it on the main thread
.flowOn(sessionDispatcher)
.launchIn(sessionCoroutineScope) .launchIn(sessionCoroutineScope)
} }
} }
@@ -158,6 +158,7 @@ class JoinedRustRoom(
inner = innerRoom.threadListService(), inner = innerRoom.threadListService(),
contentMapper = TimelineEventContentMapper(), contentMapper = TimelineEventContentMapper(),
roomCoroutineScope = roomCoroutineScope, roomCoroutineScope = roomCoroutineScope,
roomDispatcher = roomDispatcher,
) )
override val syncUpdateFlow = flow { override val syncUpdateFlow = flow {
@@ -17,10 +17,12 @@ import io.element.android.libraries.matrix.api.room.threads.ThreadsListService
import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper
import io.element.android.libraries.matrix.impl.timeline.item.event.map import io.element.android.libraries.matrix.impl.timeline.item.event.map
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.util.mxCallbackFlow
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
@@ -33,6 +35,7 @@ import org.matrix.rustcomponents.sdk.ThreadListService as InnerThreadListService
class RustThreadsListService( class RustThreadsListService(
private val inner: InnerThreadListService, private val inner: InnerThreadListService,
private val roomCoroutineScope: CoroutineScope, private val roomCoroutineScope: CoroutineScope,
private val roomDispatcher: CoroutineDispatcher,
private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(), private val contentMapper: TimelineEventContentMapper = TimelineEventContentMapper(),
) : ThreadsListService { ) : ThreadsListService {
private var itemSubscriptionJob: Job? = null private var itemSubscriptionJob: Job? = null
@@ -63,6 +66,8 @@ class RustThreadsListService(
updated.apply(diff, contentMapper) updated.apply(diff, contentMapper)
items.value = updated items.value = updated
} }
// Fetching and mapping the items can be costly, do not do it on the main thread
.flowOn(roomDispatcher)
.launchIn(roomCoroutineScope) .launchIn(roomCoroutineScope)
} }
@@ -33,7 +33,9 @@ import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.Client
@@ -41,6 +43,7 @@ import org.matrix.rustcomponents.sdk.ClientDelegate
import java.io.File import java.io.File
import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService import org.matrix.rustcomponents.sdk.SyncService as ClientSyncService
@OptIn(ExperimentalCoroutinesApi::class)
class RustMatrixClientFactoryTest { class RustMatrixClientFactoryTest {
@Test @Test
fun test() = runTest { fun test() = runTest {
@@ -51,6 +54,7 @@ class RustMatrixClientFactoryTest {
val result = sut.create(aSessionData()) val result = sut.create(aSessionData())
assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org")) assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org"))
runCurrent()
// 2 workers are scheduled: one for the search backfill and one for the vacuum // 2 workers are scheduled: one for the search backfill and one for the vacuum
scheduleWorkersLambda.assertions().isCalledExactly(2) scheduleWorkersLambda.assertions().isCalledExactly(2)
result.destroy() result.destroy()
@@ -18,6 +18,7 @@ import io.element.android.libraries.matrix.test.A_TIMESTAMP
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.lambdaRecorder
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@@ -122,6 +123,7 @@ class RustThreadsListServiceTest {
) = RustThreadsListService( ) = RustThreadsListService(
inner = inner, inner = inner,
roomCoroutineScope = backgroundScope, roomCoroutineScope = backgroundScope,
roomDispatcher = StandardTestDispatcher(testScheduler),
) )
private fun aRustThreadListUpdate() = ThreadListUpdate.Append( private fun aRustThreadListUpdate() = ThreadListUpdate.Append(
@@ -14,6 +14,7 @@ import android.os.PowerManager
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.di.annotations.ApplicationContext import io.element.android.libraries.di.annotations.ApplicationContext
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@@ -23,23 +24,25 @@ import io.element.android.libraries.push.impl.PushDatabase
import io.element.android.libraries.push.impl.db.PushHistory import io.element.android.libraries.push.impl.db.PushHistory
import io.element.android.libraries.push.impl.db.PushRequest import io.element.android.libraries.push.impl.db.PushRequest
import io.element.android.services.toolbox.api.systemclock.SystemClock import io.element.android.services.toolbox.api.systemclock.SystemClock
import kotlinx.coroutines.withContext
import kotlin.time.Instant import kotlin.time.Instant
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class DefaultPushHistoryService( class DefaultPushHistoryService(
private val pushDatabase: PushDatabase, private val pushDatabase: PushDatabase,
private val systemClock: SystemClock, private val systemClock: SystemClock,
private val dispatchers: CoroutineDispatchers,
@ApplicationContext context: Context, @ApplicationContext context: Context,
) : PushHistoryService { ) : PushHistoryService {
private val powerManager = context.getSystemService<PowerManager>() private val powerManager = context.getSystemService<PowerManager>()
private val packageName = context.packageName private val packageName = context.packageName
override suspend fun insertOrUpdatePushRequest(pushRequest: PushRequest): Result<Unit> { override suspend fun insertOrUpdatePushRequest(pushRequest: PushRequest): Result<Unit> = withContext(dispatchers.io) {
return runCatchingExceptions { pushDatabase.pushRequestQueries.insertPushRequest(pushRequest).await() } runCatchingExceptions<Unit> { pushDatabase.pushRequestQueries.insertPushRequest(pushRequest).await() }
} }
override suspend fun insertOrUpdatePushRequests(pushRequests: List<PushRequest>): Result<Unit> { override suspend fun insertOrUpdatePushRequests(pushRequests: List<PushRequest>): Result<Unit> = withContext(dispatchers.io) {
return runCatchingExceptions { runCatchingExceptions {
pushDatabase.transaction { pushDatabase.transaction {
for (request in pushRequests) { for (request in pushRequests) {
pushDatabase.pushRequestQueries.insertPushRequest(request) pushDatabase.pushRequestQueries.insertPushRequest(request)
@@ -48,8 +51,8 @@ class DefaultPushHistoryService(
} }
} }
override suspend fun getPendingPushRequests(sessionId: SessionId, since: Instant?): Result<List<PushRequest>> { override suspend fun getPendingPushRequests(sessionId: SessionId, since: Instant?): Result<List<PushRequest>> = withContext(dispatchers.io) {
return runCatchingExceptions { runCatchingExceptions {
pushDatabase.transactionWithResult { pushDatabase.transactionWithResult {
val sinceTimeMillis = since?.toEpochMilliseconds() ?: 0 val sinceTimeMillis = since?.toEpochMilliseconds() ?: 0
pushDatabase.pushRequestQueries.selectAllPendingForSession(sessionId.value, sinceTimeMillis).executeAsList() pushDatabase.pushRequestQueries.selectAllPendingForSession(sessionId.value, sinceTimeMillis).executeAsList()
@@ -57,14 +60,14 @@ class DefaultPushHistoryService(
} }
} }
override suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit> { override suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit> = withContext(dispatchers.io) {
return runCatchingExceptions { runCatchingExceptions<Unit> {
val keepAmount = 100L val keepAmount = 100L
pushDatabase.pushRequestQueries.removeOldest(keepAmount) pushDatabase.pushRequestQueries.removeOldest(keepAmount)
} }
} }
override fun onPushResult( override suspend fun onPushResult(
providerInfo: String, providerInfo: String,
eventId: EventId?, eventId: EventId?,
roomId: RoomId?, roomId: RoomId?,
@@ -91,19 +94,21 @@ class DefaultPushHistoryService(
} }
}.takeIf { it.isNotEmpty() } }.takeIf { it.isNotEmpty() }
pushDatabase.pushHistoryQueries.insertPushHistory( withContext(dispatchers.io) {
PushHistory( pushDatabase.pushHistoryQueries.insertPushHistory(
pushDate = systemClock.epochMillis(), PushHistory(
providerInfo = providerInfo, pushDate = systemClock.epochMillis(),
eventId = eventId?.value, providerInfo = providerInfo,
roomId = roomId?.value, eventId = eventId?.value,
sessionId = sessionId?.value, roomId = roomId?.value,
hasBeenResolved = if (hasBeenResolved) 1 else 0, sessionId = sessionId?.value,
comment = finalComment, hasBeenResolved = if (hasBeenResolved) 1 else 0,
comment = finalComment,
)
) )
)
// Keep only the last 1_000 events // Keep only the last 1_000 events
pushDatabase.pushHistoryQueries.removeOldest(1_000) pushDatabase.pushHistoryQueries.removeOldest(1_000)
}
} }
} }
@@ -20,7 +20,7 @@ interface PushHistoryService {
* Create a new push history entry. * Create a new push history entry.
* Do not use directly, prefer using the extension functions. * Do not use directly, prefer using the extension functions.
*/ */
fun onPushResult( suspend fun onPushResult(
providerInfo: String, providerInfo: String,
eventId: EventId?, eventId: EventId?,
roomId: RoomId?, roomId: RoomId?,
@@ -52,7 +52,7 @@ interface PushHistoryService {
suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit> suspend fun removeOldPushRequests(sessionId: SessionId): Result<Unit>
} }
fun PushHistoryService.onInvalidPushReceived( suspend fun PushHistoryService.onInvalidPushReceived(
providerInfo: String, providerInfo: String,
data: String, data: String,
) = onPushResult( ) = onPushResult(
@@ -65,7 +65,7 @@ fun PushHistoryService.onInvalidPushReceived(
comment = "Invalid or ignored push data:\n$data", comment = "Invalid or ignored push data:\n$data",
) )
fun PushHistoryService.onUnableToRetrieveSession( suspend fun PushHistoryService.onUnableToRetrieveSession(
providerInfo: String, providerInfo: String,
eventId: EventId, eventId: EventId,
roomId: RoomId, roomId: RoomId,
@@ -80,7 +80,7 @@ fun PushHistoryService.onUnableToRetrieveSession(
comment = "Unable to retrieve session: $reason", comment = "Unable to retrieve session: $reason",
) )
fun PushHistoryService.onUnableToResolveEvent( suspend fun PushHistoryService.onUnableToResolveEvent(
providerInfo: String, providerInfo: String,
eventId: EventId, eventId: EventId,
roomId: RoomId, roomId: RoomId,
@@ -96,7 +96,7 @@ fun PushHistoryService.onUnableToResolveEvent(
comment = "Unable to resolve event: $reason", comment = "Unable to resolve event: $reason",
) )
fun PushHistoryService.onSuccess( suspend fun PushHistoryService.onSuccess(
providerInfo: String, providerInfo: String,
eventId: EventId, eventId: EventId,
roomId: RoomId, roomId: RoomId,
@@ -117,7 +117,7 @@ fun PushHistoryService.onSuccess(
}, },
) )
fun PushHistoryService.onDiagnosticPush( suspend fun PushHistoryService.onDiagnosticPush(
providerInfo: String, providerInfo: String,
) = onPushResult( ) = onPushResult(
providerInfo = providerInfo, providerInfo = providerInfo,
@@ -30,7 +30,7 @@ class FakePushHistoryService(
private val getPendingPushRequests: (SessionId, Instant?) -> Result<List<PushRequest>> = { _, _ -> lambdaError() }, private val getPendingPushRequests: (SessionId, Instant?) -> Result<List<PushRequest>> = { _, _ -> lambdaError() },
private val removeOldPushRequests: (SessionId) -> Result<Unit> = { lambdaError() }, private val removeOldPushRequests: (SessionId) -> Result<Unit> = { lambdaError() },
) : PushHistoryService { ) : PushHistoryService {
override fun onPushResult( override suspend fun onPushResult(
providerInfo: String, providerInfo: String,
eventId: EventId?, eventId: EventId?,
roomId: RoomId?, roomId: RoomId?,