diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/VacuumStoresUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/VacuumStoresUseCase.kt index 0e6068cb4d..1d0de56f09 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/VacuumStoresUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/VacuumStoresUseCase.kt @@ -12,7 +12,7 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.matrix.api.MatrixClient import timber.log.Timber -interface VacuumStoresUseCase { +fun interface VacuumStoresUseCase { suspend operator fun invoke() } @@ -21,7 +21,7 @@ class DefaultVacuumStoresUseCase( private val matrixClient: MatrixClient, ) : VacuumStoresUseCase { override suspend fun invoke() { - matrixClient.vacuumStores() + matrixClient.performDatabaseVacuum() .onFailure { Timber.e(it, "Failed to vacuum stores") } } } diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index fe2d8445fd..75ae12b714 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -17,6 +17,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase +import io.element.android.features.preferences.impl.tasks.VacuumStoresUseCase import io.element.android.features.rageshake.api.preferences.aRageshakePreferencesState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -212,6 +213,23 @@ class DeveloperSettingsPresenterTest { } } + @Test + fun `present - VacuumStores action invokes the VacuumStoresUseCase`() = runTest { + var vacuumCalled = false + val presenter = createDeveloperSettingsPresenter( + vacuumStoresUseCase = VacuumStoresUseCase { + vacuumCalled = true + } + ) + presenter.test { + val state = awaitItem() + assertThat(vacuumCalled).isFalse() + state.eventSink(DeveloperSettingsEvents.VacuumStores) + skipItems(1) + assertThat(vacuumCalled).isTrue() + } + } + private fun createDeveloperSettingsPresenter( sessionId: SessionId = A_SESSION_ID, featureFlagService: FakeFeatureFlagService = FakeFeatureFlagService( @@ -230,6 +248,7 @@ class DeveloperSettingsPresenterTest { preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), buildMeta: BuildMeta = aBuildMeta(), enterpriseService: EnterpriseService = FakeEnterpriseService(), + vacuumStoresUseCase: VacuumStoresUseCase = VacuumStoresUseCase {}, ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( sessionId = sessionId, @@ -240,6 +259,7 @@ class DeveloperSettingsPresenterTest { appPreferencesStore = preferencesStore, buildMeta = buildMeta, enterpriseService = enterpriseService, + vacuumStoresUseCase = vacuumStoresUseCase, ) } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 566434b6be..ae803110d5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -195,7 +195,7 @@ interface MatrixClient { */ suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result - suspend fun vacuumStores(): Result + suspend fun performDatabaseVacuum(): Result } /** diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index 394cb2398e..6edf26008a 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.network) implementation(projects.libraries.preferences.api) + implementation(projects.libraries.workmanager.api) implementation(projects.services.analytics.api) implementation(projects.services.toolbox.api) api(projects.libraries.matrix.api) @@ -49,6 +50,7 @@ dependencies { testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.previewutils) testImplementation(projects.libraries.sessionStorage.test) + testImplementation(projects.libraries.workmanager.test) testImplementation(projects.services.analytics.test) testImplementation(projects.services.toolbox.test) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 31e80cf442..8784a69d7c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -75,7 +75,10 @@ import io.element.android.libraries.matrix.impl.util.SessionPathsProvider import io.element.android.libraries.matrix.impl.util.cancelAndDestroy import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.verification.RustSessionVerificationService +import io.element.android.libraries.matrix.impl.workmanager.PerformDatabaseVacuumWorkManagerRequest import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.workmanager.api.WorkManagerRequestType +import io.element.android.libraries.workmanager.api.WorkManagerScheduler import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.collections.immutable.ImmutableList @@ -133,6 +136,7 @@ class RustMatrixClient( timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val featureFlagService: FeatureFlagService, private val analyticsService: AnalyticsService, + private val workManagerScheduler: WorkManagerScheduler, ) : MatrixClient { override val sessionId: UserId = UserId(innerClient.userId()) override val deviceId: DeviceId = DeviceId(innerClient.deviceId()) @@ -276,6 +280,9 @@ class RustMatrixClient( // Force a refresh of the profile getUserProfile() } + + // Schedule regular database vacuuming to ensure DB performance remains optimal + scheduleDatabaseVacuum() } override fun userIdServerName(): String { @@ -726,8 +733,9 @@ class RustMatrixClient( } } - override suspend fun vacuumStores(): Result = withContext(sessionDispatcher) { + override suspend fun performDatabaseVacuum(): Result = withContext(sessionDispatcher) { runCatchingExceptions { + Timber.d("Performing database vacuuming for session $sessionId...") innerClient.optimizeStores() } } @@ -756,6 +764,15 @@ class RustMatrixClient( // Delete all the files for this session sessionPathsProvider.provides(sessionId)?.deleteRecursively() } + + private fun scheduleDatabaseVacuum() { + // If there's already a periodic work request, do not schedule another one + if (workManagerScheduler.hasPendingWork(sessionId, WorkManagerRequestType.DB_VACUUM)) return + + Timber.i("Scheduling periodic database vacuuming for session $sessionId") + val request = PerformDatabaseVacuumWorkManagerRequest(sessionId) + workManagerScheduler.submit(request) + } } private val defaultRoomCreationPowerLevels = PowerLevels( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 58e3485279..3d94f7554d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.impl.util.anonymizedTokens import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.workmanager.api.WorkManagerScheduler import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope @@ -63,6 +64,7 @@ class RustMatrixClientFactory( private val timelineEventTypeFilterFactory: TimelineEventTypeFilterFactory, private val clientBuilderProvider: ClientBuilderProvider, private val sqliteStoreBuilderProvider: SqliteStoreBuilderProvider, + private val workManagerScheduler: WorkManagerScheduler, ) { private val sessionDelegate = RustClientSessionDelegate(sessionStore, appCoroutineScope, coroutineDispatchers) @@ -116,6 +118,7 @@ class RustMatrixClientFactory( timelineEventTypeFilterFactory = timelineEventTypeFilterFactory, featureFlagService = featureFlagService, analyticsService = analyticsService, + workManagerScheduler = workManagerScheduler, ).also { Timber.tag(it.toString()).d("Creating Client with access token '$anonymizedAccessToken' and refresh token '$anonymizedRefreshToken'") } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/workmanager/PerformDatabaseVacuumWorkManagerRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/workmanager/PerformDatabaseVacuumWorkManagerRequest.kt new file mode 100644 index 0000000000..9c192bd96d --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/workmanager/PerformDatabaseVacuumWorkManagerRequest.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 Element Creations 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.matrix.impl.workmanager + +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkRequest +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.impl.workmanager.VacuumDatabaseWorker.Companion.SESSION_ID_PARAM +import io.element.android.libraries.workmanager.api.WorkManagerRequest +import io.element.android.libraries.workmanager.api.WorkManagerRequestType +import io.element.android.libraries.workmanager.api.workManagerTag +import java.util.concurrent.TimeUnit + +class PerformDatabaseVacuumWorkManagerRequest( + private val sessionId: SessionId, +) : WorkManagerRequest { + override fun build(): Result> { + val data = Data.Builder().putString(SESSION_ID_PARAM, sessionId.value).build() + val workRequest = PeriodicWorkRequest.Builder( + workerClass = VacuumDatabaseWorker::class, + // Run once a day + repeatInterval = 1, + repeatIntervalTimeUnit = TimeUnit.DAYS, + ) + .addTag(workManagerTag(sessionId, WorkManagerRequestType.DB_VACUUM)) + .setInputData(data) + // Only run when the device is idle to avoid impacting user experience + .setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build()) + .build() + + return Result.success(listOf(workRequest)) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/workmanager/VacuumDatabaseWorker.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/workmanager/VacuumDatabaseWorker.kt new file mode 100644 index 0000000000..0602c2fdc2 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/workmanager/VacuumDatabaseWorker.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Element Creations 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.matrix.impl.workmanager + +import android.content.Context +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import dev.zacsweers.metro.Assisted +import dev.zacsweers.metro.AssistedInject +import io.element.android.libraries.di.annotations.ApplicationContext +import io.element.android.libraries.matrix.api.MatrixClientProvider +import io.element.android.libraries.matrix.api.core.SessionId + +@AssistedInject +class VacuumDatabaseWorker( + @Assisted workerParams: WorkerParameters, + @ApplicationContext private val context: Context, + private val matrixClientProvider: MatrixClientProvider, +) : CoroutineWorker(context, workerParams) { + companion object { + const val SESSION_ID_PARAM = "session_id" + } + + override suspend fun doWork(): Result { + val sessionId = inputData.getString(SESSION_ID_PARAM)?.let(::SessionId) ?: return Result.failure() + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return Result.failure() + return client.performDatabaseVacuum() + .fold( + onSuccess = { Result.success() }, + onFailure = { Result.failure() } + ) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index a1b52d430a..fa69752afe 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -19,8 +19,11 @@ import io.element.android.libraries.network.useragent.SimpleUserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.libraries.workmanager.api.WorkManagerRequest +import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.systemclock.FakeSystemClock +import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -30,9 +33,14 @@ import java.io.File class RustMatrixClientFactoryTest { @Test fun test() = runTest { - val sut = createRustMatrixClientFactory() + val scheduleVacuumLambda = lambdaRecorder {} + val workManagerScheduler = FakeWorkManagerScheduler(submitLambda = scheduleVacuumLambda) + val sut = createRustMatrixClientFactory(workManagerScheduler = workManagerScheduler) + val result = sut.create(aSessionData()) + assertThat(result.sessionId).isEqualTo(SessionId("@alice:server.org")) + scheduleVacuumLambda.assertions().isCalledOnce() result.destroy() } } @@ -43,6 +51,7 @@ fun TestScope.createRustMatrixClientFactory( updateUserProfileResult = { _, _, _ -> }, ), clientBuilderProvider: ClientBuilderProvider = FakeClientBuilderProvider(), + workManagerScheduler: FakeWorkManagerScheduler = FakeWorkManagerScheduler(), ) = RustMatrixClientFactory( cacheDirectory = cacheDirectory, appCoroutineScope = backgroundScope, @@ -57,4 +66,5 @@ fun TestScope.createRustMatrixClientFactory( timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(), clientBuilderProvider = clientBuilderProvider, sqliteStoreBuilderProvider = FakeSqliteStoreBuilderProvider(), + workManagerScheduler = workManagerScheduler, ) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt index c81d9f1c15..3c8619ed72 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientTest.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData +import io.element.android.libraries.workmanager.test.FakeWorkManagerScheduler import io.element.android.services.analytics.test.FakeAnalyticsService import io.element.android.services.toolbox.test.systemclock.FakeSystemClock import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -116,5 +117,6 @@ class RustMatrixClientTest { timelineEventTypeFilterFactory = FakeTimelineEventTypeFilterFactory(), featureFlagService = FakeFeatureFlagService(), analyticsService = FakeAnalyticsService(), + workManagerScheduler = FakeWorkManagerScheduler(submitLambda = {}), ) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 940cce6164..38d4d1aefe 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -104,6 +104,7 @@ class FakeMatrixClient( private val getRecentEmojisLambda: () -> Result> = { Result.success(emptyList()) }, private val addRecentEmojiLambda: (String) -> Result = { Result.success(Unit) }, private val markRoomAsFullyReadResult: (RoomId, EventId) -> Result = { _, _ -> lambdaError() }, + private val performDatabaseVacuumLambda: () -> Result = { lambdaError() }, ) : MatrixClient { var setDisplayNameCalled: Boolean = false private set @@ -351,4 +352,8 @@ class FakeMatrixClient( override suspend fun markRoomAsFullyRead(roomId: RoomId, eventId: EventId): Result { return markRoomAsFullyReadResult(roomId, eventId) } + + override suspend fun performDatabaseVacuum(): Result { + return performDatabaseVacuumLambda() + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt index 0d1478cb9e..b40b3fe79f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationResolverQueue.kt @@ -17,7 +17,7 @@ import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.push.api.push.NotificationEventRequest import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest -import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter +import io.element.android.libraries.push.impl.workmanager.SyncNotificationsWorkerDataConverter import io.element.android.libraries.workmanager.api.WorkManagerScheduler import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider import kotlinx.coroutines.CoroutineScope @@ -50,7 +50,7 @@ class DefaultNotificationResolverQueue( private val appCoroutineScope: CoroutineScope, private val workManagerScheduler: WorkManagerScheduler, private val featureFlagService: FeatureFlagService, - private val workerDataConverter: WorkerDataConverter, + private val workerDataConverter: SyncNotificationsWorkerDataConverter, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, ) : NotificationResolverQueue { companion object { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationsWorker.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationsWorker.kt index ccc0a02749..eeac5ff66f 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationsWorker.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationsWorker.kt @@ -49,7 +49,7 @@ class FetchNotificationsWorker( private val workManagerScheduler: WorkManagerScheduler, private val syncOnNotifiableEvent: SyncOnNotifiableEvent, private val coroutineDispatchers: CoroutineDispatchers, - private val workerDataConverter: WorkerDataConverter, + private val workerDataConverter: SyncNotificationsWorkerDataConverter, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequest.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequest.kt index b11b83d6e4..50ef28903c 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequest.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequest.kt @@ -26,7 +26,7 @@ import java.security.InvalidParameterException class SyncNotificationWorkManagerRequest( private val sessionId: SessionId, private val notificationEventRequests: List, - private val workerDataConverter: WorkerDataConverter, + private val workerDataConverter: SyncNotificationsWorkerDataConverter, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, ) : WorkManagerRequest { override fun build(): Result> { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverter.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationsWorkerDataConverter.kt similarity index 99% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverter.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationsWorkerDataConverter.kt index 23e66396c6..46b7d760c0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverter.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationsWorkerDataConverter.kt @@ -21,7 +21,7 @@ import io.element.android.libraries.push.api.push.NotificationEventRequest import timber.log.Timber @Inject -class WorkerDataConverter( +class SyncNotificationsWorkerDataConverter( private val json: JsonProvider, ) { fun serialize(notificationEventRequests: List): Result> { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt index 89b8c0d319..7c95034985 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandlerTest.kt @@ -47,7 +47,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven import io.element.android.libraries.push.impl.notifications.model.ResolvedPushEvent import io.element.android.libraries.push.impl.test.DefaultTestPush import io.element.android.libraries.push.impl.troubleshoot.DiagnosticPushHandler -import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter +import io.element.android.libraries.push.impl.workmanager.SyncNotificationsWorkerDataConverter import io.element.android.libraries.pushproviders.api.PushData import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret @@ -718,7 +718,7 @@ class DefaultPushHandlerTest { appCoroutineScope = backgroundScope, workManagerScheduler = workManagerScheduler, featureFlagService = featureFlagService, - workerDataConverter = WorkerDataConverter(DefaultJsonProvider()), + workerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()), buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33), ), appCoroutineScope = backgroundScope, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationWorkerTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationWorkerTest.kt index 23a38db66c..d40ef17b53 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationWorkerTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/FetchNotificationWorkerTest.kt @@ -177,7 +177,7 @@ class FetchNotificationWorkerTest { workManagerScheduler = workManagerScheduler, syncOnNotifiableEvent = syncOnNotifiableEvent, coroutineDispatchers = testCoroutineDispatchers(), - workerDataConverter = WorkerDataConverter(DefaultJsonProvider()), + workerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()), buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(33), ) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequestTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequestTest.kt index 9dae435a9a..1f8d646e2b 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequestTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/SyncNotificationWorkManagerRequestTest.kt @@ -78,7 +78,7 @@ class SyncNotificationWorkManagerRequestTest { val request = createSyncNotificationWorkManagerRequest( sessionId = A_SESSION_ID, notificationEventRequests = listOf(aNotificationEventRequest()), - workerDataConverter = WorkerDataConverter({ error("error during serialization") }) + workerDataConverter = SyncNotificationsWorkerDataConverter({ error("error during serialization") }) ) val result = request.build() assertThat(result.isFailure).isTrue() @@ -88,7 +88,7 @@ class SyncNotificationWorkManagerRequestTest { private fun createSyncNotificationWorkManagerRequest( sessionId: SessionId, notificationEventRequests: List, - workerDataConverter: WorkerDataConverter = WorkerDataConverter(DefaultJsonProvider()), + workerDataConverter: SyncNotificationsWorkerDataConverter = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()), sdkVersion: Int = 33, ) = SyncNotificationWorkManagerRequest( sessionId = sessionId, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverterTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverterTest.kt index 6c6998cb35..85b55e0d62 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverterTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/workmanager/WorkerDataConverterTest.kt @@ -57,10 +57,10 @@ class WorkerDataConverterTest { providerInfo = "info$it", ) } - val sut = WorkerDataConverter(DefaultJsonProvider()) + val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()) val serialized = sut.serialize(data) assertThat(serialized.getOrNull()?.size).isGreaterThan(1) - assertThat(serialized.getOrNull()?.size).isEqualTo(100 / WorkerDataConverter.CHUNK_SIZE) + assertThat(serialized.getOrNull()?.size).isEqualTo(100 / SyncNotificationsWorkerDataConverter.CHUNK_SIZE) // All the items are present val deserialized = serialized.getOrNull()?.flatMap { sut.deserialize(it)!! } assertThat(deserialized).containsExactlyElementsIn(data) @@ -76,10 +76,10 @@ class WorkerDataConverterTest { providerInfo = "info$it", ) } - val sut = WorkerDataConverter(DefaultJsonProvider()) + val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()) val serialized = sut.serialize(data) assertThat(serialized.getOrNull()?.size).isGreaterThan(1) - assertThat(serialized.getOrNull()?.size).isEqualTo(100 / WorkerDataConverter.CHUNK_SIZE + 1) + assertThat(serialized.getOrNull()?.size).isEqualTo(100 / SyncNotificationsWorkerDataConverter.CHUNK_SIZE + 1) // All the items are present val deserialized = serialized.getOrNull()?.flatMap { sut.deserialize(it)!! } assertThat(deserialized).containsExactlyElementsIn(data) @@ -112,7 +112,7 @@ class WorkerDataConverterTest { ) } val data = (data1 + data2 + data3).shuffled() - val sut = WorkerDataConverter(DefaultJsonProvider()) + val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()) val serialized = sut.serialize(data) assertThat(serialized.getOrNull()?.size).isEqualTo(2) // All the items are present @@ -133,7 +133,7 @@ class WorkerDataConverterTest { } private fun testIdentity(data: List) { - val sut = WorkerDataConverter(DefaultJsonProvider()) + val sut = SyncNotificationsWorkerDataConverter(DefaultJsonProvider()) val serialized = sut.serialize(data).getOrThrow() val result = sut.deserialize(serialized.first()) assertThat(result).isEqualTo(data) diff --git a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt index b385f0b2fb..b538486d35 100644 --- a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt +++ b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerScheduler.kt @@ -12,16 +12,19 @@ import io.element.android.libraries.matrix.api.core.SessionId interface WorkManagerScheduler { fun submit(workManagerRequest: WorkManagerRequest) + fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean fun cancel(sessionId: SessionId) } fun workManagerTag(sessionId: SessionId, requestType: WorkManagerRequestType): String { val prefix = when (requestType) { WorkManagerRequestType.NOTIFICATION_SYNC -> "notifications" + WorkManagerRequestType.DB_VACUUM -> "db_vacuum" } return "$prefix-$sessionId" } enum class WorkManagerRequestType { NOTIFICATION_SYNC, + DB_VACUUM, } diff --git a/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/DefaultWorkManagerScheduler.kt b/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/DefaultWorkManagerScheduler.kt index e5bd67c604..9a96d2ae73 100644 --- a/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/DefaultWorkManagerScheduler.kt +++ b/libraries/workmanager/impl/src/main/kotlin/io/element/android/libraries/workmanager/impl/DefaultWorkManagerScheduler.kt @@ -11,6 +11,7 @@ package io.element.android.libraries.workmanager.impl import androidx.work.WorkManager import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.bool.orFalse import dev.zacsweers.metro.SingleIn import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.sessionstorage.api.observer.SessionListener @@ -51,6 +52,10 @@ class DefaultWorkManagerScheduler( ) } + override fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean { + return workManager.getWorkInfosByTag(workManagerTag(sessionId, requestType)).get()?.isNotEmpty().orFalse() + } + override fun cancel(sessionId: SessionId) { Timber.d("Cancelling work for sessionId: $sessionId") for (requestType in WorkManagerRequestType.entries) { diff --git a/libraries/workmanager/test/src/main/kotlin/io/element/android/libraries/workmanager/test/FakeWorkManagerScheduler.kt b/libraries/workmanager/test/src/main/kotlin/io/element/android/libraries/workmanager/test/FakeWorkManagerScheduler.kt index 94ee826ddc..f2caa8c743 100644 --- a/libraries/workmanager/test/src/main/kotlin/io/element/android/libraries/workmanager/test/FakeWorkManagerScheduler.kt +++ b/libraries/workmanager/test/src/main/kotlin/io/element/android/libraries/workmanager/test/FakeWorkManagerScheduler.kt @@ -10,17 +10,23 @@ package io.element.android.libraries.workmanager.test import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.workmanager.api.WorkManagerRequest +import io.element.android.libraries.workmanager.api.WorkManagerRequestType import io.element.android.libraries.workmanager.api.WorkManagerScheduler import io.element.android.tests.testutils.lambda.lambdaError class FakeWorkManagerScheduler( private val submitLambda: (WorkManagerRequest) -> Unit = { lambdaError() }, + private val hasPendingWorkLambda: (SessionId, WorkManagerRequestType) -> Boolean = { _, _ -> false }, private val cancelLambda: (SessionId) -> Unit = { lambdaError() }, ) : WorkManagerScheduler { override fun submit(workManagerRequest: WorkManagerRequest) { submitLambda(workManagerRequest) } + override fun hasPendingWork(sessionId: SessionId, requestType: WorkManagerRequestType): Boolean { + return hasPendingWorkLambda(sessionId, requestType) + } + override fun cancel(sessionId: SessionId) { cancelLambda(sessionId) }