mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Merge pull request #7901 from element-hq/feature/bma/mainThreadBlocking
Avoid blocking the main thread in several places
This commit is contained in:
+51
-38
@@ -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() {
|
||||||
|
|||||||
+1
@@ -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
|
||||||
|
|||||||
+1
@@ -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
|
||||||
|
|||||||
+13
-7
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
@@ -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),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -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 {
|
||||||
|
|||||||
+5
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
@@ -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()
|
||||||
|
|||||||
+2
@@ -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(
|
||||||
|
|||||||
+26
-21
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -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,
|
||||||
|
|||||||
+1
-1
@@ -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?,
|
||||||
|
|||||||
Reference in New Issue
Block a user