Introduce WorkerDataConverter to avoid hard coded Json key and ensure serializing/deserializing is performed at the same place.
This commit is contained in:
parent
4861ae5171
commit
9983871db7
8 changed files with 140 additions and 52 deletions
|
|
@ -11,13 +11,13 @@ import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
import dev.zacsweers.metro.Inject
|
import dev.zacsweers.metro.Inject
|
||||||
import dev.zacsweers.metro.SingleIn
|
import dev.zacsweers.metro.SingleIn
|
||||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
|
||||||
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
import io.element.android.libraries.di.annotations.AppCoroutineScope
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
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.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
|
import io.element.android.libraries.push.impl.workmanager.SyncNotificationWorkManagerRequest
|
||||||
|
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
import io.element.android.libraries.workmanager.api.WorkManagerScheduler
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
|
@ -50,7 +50,7 @@ class DefaultNotificationResolverQueue(
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
private val workManagerScheduler: WorkManagerScheduler,
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val json: JsonProvider,
|
private val workerDataConverter: WorkerDataConverter,
|
||||||
) : NotificationResolverQueue {
|
) : NotificationResolverQueue {
|
||||||
companion object {
|
companion object {
|
||||||
private const val BATCH_WINDOW_MS = 250L
|
private const val BATCH_WINDOW_MS = 250L
|
||||||
|
|
@ -101,7 +101,7 @@ class DefaultNotificationResolverQueue(
|
||||||
SyncNotificationWorkManagerRequest(
|
SyncNotificationWorkManagerRequest(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
notificationEventRequests = requests,
|
notificationEventRequests = requests,
|
||||||
json = json,
|
workerDataConverter = workerDataConverter,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import dev.zacsweers.metro.ContributesIntoMap
|
||||||
import dev.zacsweers.metro.binding
|
import dev.zacsweers.metro.binding
|
||||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
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
|
||||||
|
|
@ -47,20 +46,11 @@ class FetchNotificationsWorker(
|
||||||
private val workManagerScheduler: WorkManagerScheduler,
|
private val workManagerScheduler: WorkManagerScheduler,
|
||||||
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
private val syncOnNotifiableEvent: SyncOnNotifiableEvent,
|
||||||
private val coroutineDispatchers: CoroutineDispatchers,
|
private val coroutineDispatchers: CoroutineDispatchers,
|
||||||
private val json: JsonProvider,
|
private val workerDataConverter: WorkerDataConverter,
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
|
override suspend fun doWork(): Result = withContext(coroutineDispatchers.io) {
|
||||||
Timber.d("FetchNotificationsWorker started")
|
Timber.d("FetchNotificationsWorker started")
|
||||||
val rawRequestsJson = inputData.getString("requests") ?: return@withContext Result.failure()
|
val requests = workerDataConverter.deserialize(inputData) ?: return@withContext Result.failure()
|
||||||
val requests = runCatchingExceptions {
|
|
||||||
json().decodeFromString<List<SyncNotificationWorkManagerRequest.Data>>(rawRequestsJson).map { it.toRequest() }
|
|
||||||
}.getOrElse {
|
|
||||||
Timber.e(it, "Failed to deserialize notification requests")
|
|
||||||
return@withContext Result.failure()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("Deserialized ${requests.size} requests")
|
|
||||||
|
|
||||||
// Wait for network to be available, but not more than 10 seconds
|
// Wait for network to be available, but not more than 10 seconds
|
||||||
val hasNetwork = withTimeoutOrNull(10.seconds) {
|
val hasNetwork = withTimeoutOrNull(10.seconds) {
|
||||||
networkMonitor.connectivity.first { it == NetworkStatus.Connected }
|
networkMonitor.connectivity.first { it == NetworkStatus.Connected }
|
||||||
|
|
@ -97,7 +87,7 @@ class FetchNotificationsWorker(
|
||||||
SyncNotificationWorkManagerRequest(
|
SyncNotificationWorkManagerRequest(
|
||||||
sessionId = failedSessionId,
|
sessionId = failedSessionId,
|
||||||
notificationEventRequests = requestsToRetry,
|
notificationEventRequests = requestsToRetry,
|
||||||
json = json,
|
workerDataConverter = workerDataConverter,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,6 @@ package io.element.android.libraries.push.impl.workmanager
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.OutOfQuotaPolicy
|
import androidx.work.OutOfQuotaPolicy
|
||||||
import androidx.work.WorkRequest
|
import androidx.work.WorkRequest
|
||||||
import androidx.work.workDataOf
|
|
||||||
import io.element.android.libraries.androidutils.json.JsonProvider
|
|
||||||
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
import io.element.android.libraries.workmanager.api.WorkManagerRequest
|
||||||
|
|
@ -28,24 +23,19 @@ import java.security.InvalidParameterException
|
||||||
class SyncNotificationWorkManagerRequest(
|
class SyncNotificationWorkManagerRequest(
|
||||||
private val sessionId: SessionId,
|
private val sessionId: SessionId,
|
||||||
private val notificationEventRequests: List<NotificationEventRequest>,
|
private val notificationEventRequests: List<NotificationEventRequest>,
|
||||||
private val json: JsonProvider,
|
private val workerDataConverter: WorkerDataConverter,
|
||||||
) : WorkManagerRequest {
|
) : WorkManagerRequest {
|
||||||
override fun build(): Result<WorkRequest> {
|
override fun build(): Result<WorkRequest> {
|
||||||
if (notificationEventRequests.isEmpty()) {
|
if (notificationEventRequests.isEmpty()) {
|
||||||
return Result.failure(InvalidParameterException("notificationEventRequests cannot be empty"))
|
return Result.failure(InvalidParameterException("notificationEventRequests cannot be empty"))
|
||||||
}
|
}
|
||||||
|
val data = workerDataConverter.serialize(notificationEventRequests).getOrElse {
|
||||||
val json = runCatchingExceptions { json().encodeToString(notificationEventRequests.map { it.toData() }) }
|
return Result.failure(it)
|
||||||
.getOrElse {
|
}
|
||||||
Timber.e(it, "Failed to serialize notification requests")
|
|
||||||
return Result.failure(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("Scheduling ${notificationEventRequests.size} notification requests with WorkManager for $sessionId")
|
Timber.d("Scheduling ${notificationEventRequests.size} notification requests with WorkManager for $sessionId")
|
||||||
|
|
||||||
return Result.success(
|
return Result.success(
|
||||||
OneTimeWorkRequestBuilder<FetchNotificationsWorker>()
|
OneTimeWorkRequestBuilder<FetchNotificationsWorker>()
|
||||||
.setInputData(workDataOf("requests" to json))
|
.setInputData(data)
|
||||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||||
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
|
.setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC))
|
||||||
// TODO investigate using this instead of the resolver queue
|
// TODO investigate using this instead of the resolver queue
|
||||||
|
|
@ -64,23 +54,5 @@ class SyncNotificationWorkManagerRequest(
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
@SerialName("provider_info")
|
@SerialName("provider_info")
|
||||||
val providerInfo: String,
|
val providerInfo: String,
|
||||||
) {
|
|
||||||
fun toRequest(): NotificationEventRequest {
|
|
||||||
return NotificationEventRequest(
|
|
||||||
sessionId = SessionId(sessionId),
|
|
||||||
roomId = RoomId(roomId),
|
|
||||||
eventId = EventId(eventId),
|
|
||||||
providerInfo = providerInfo,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun NotificationEventRequest.toData(): SyncNotificationWorkManagerRequest.Data {
|
|
||||||
return SyncNotificationWorkManagerRequest.Data(
|
|
||||||
sessionId = sessionId.value,
|
|
||||||
roomId = roomId.value,
|
|
||||||
eventId = eventId.value,
|
|
||||||
providerInfo = providerInfo,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.workmanager
|
||||||
|
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import dev.zacsweers.metro.Inject
|
||||||
|
import io.element.android.libraries.androidutils.json.JsonProvider
|
||||||
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
class WorkerDataConverter(
|
||||||
|
private val json: JsonProvider,
|
||||||
|
) {
|
||||||
|
fun serialize(notificationEventRequests: List<NotificationEventRequest>): Result<Data> {
|
||||||
|
return runCatchingExceptions { json().encodeToString(notificationEventRequests.map { it.toData() }) }
|
||||||
|
.onFailure {
|
||||||
|
Timber.e(it, "Failed to serialize notification requests")
|
||||||
|
}
|
||||||
|
.map { str ->
|
||||||
|
workDataOf(REQUESTS_KEY to str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(data: Data): List<NotificationEventRequest>? {
|
||||||
|
val rawRequestsJson = data.getString(REQUESTS_KEY) ?: return null
|
||||||
|
return runCatchingExceptions {
|
||||||
|
json().decodeFromString<List<SyncNotificationWorkManagerRequest.Data>>(rawRequestsJson).map { it.toRequest() }
|
||||||
|
}.fold(
|
||||||
|
onSuccess = {
|
||||||
|
Timber.d("Deserialized ${it.size} requests")
|
||||||
|
it
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
Timber.e(it, "Failed to deserialize notification requests")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val REQUESTS_KEY = "requests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationEventRequest.toData(): SyncNotificationWorkManagerRequest.Data {
|
||||||
|
return SyncNotificationWorkManagerRequest.Data(
|
||||||
|
sessionId = sessionId.value,
|
||||||
|
roomId = roomId.value,
|
||||||
|
eventId = eventId.value,
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun SyncNotificationWorkManagerRequest.Data.toRequest(): NotificationEventRequest {
|
||||||
|
return NotificationEventRequest(
|
||||||
|
sessionId = SessionId(sessionId),
|
||||||
|
roomId = RoomId(roomId),
|
||||||
|
eventId = EventId(eventId),
|
||||||
|
providerInfo = providerInfo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -46,6 +46,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.notifications.model.ResolvedPushEvent
|
||||||
import io.element.android.libraries.push.impl.test.DefaultTestPush
|
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.troubleshoot.DiagnosticPushHandler
|
||||||
|
import io.element.android.libraries.push.impl.workmanager.WorkerDataConverter
|
||||||
import io.element.android.libraries.pushproviders.api.PushData
|
import io.element.android.libraries.pushproviders.api.PushData
|
||||||
import io.element.android.libraries.pushstore.api.UserPushStore
|
import io.element.android.libraries.pushstore.api.UserPushStore
|
||||||
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret
|
||||||
|
|
@ -715,7 +716,7 @@ class DefaultPushHandlerTest {
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
featureFlagService = featureFlagService,
|
featureFlagService = featureFlagService,
|
||||||
json = DefaultJsonProvider(),
|
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
||||||
),
|
),
|
||||||
appCoroutineScope = backgroundScope,
|
appCoroutineScope = backgroundScope,
|
||||||
fallbackNotificationFactory = FallbackNotificationFactory(
|
fallbackNotificationFactory = FallbackNotificationFactory(
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ class FetchNotificationWorkerTest {
|
||||||
workManagerScheduler = workManagerScheduler,
|
workManagerScheduler = workManagerScheduler,
|
||||||
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
syncOnNotifiableEvent = syncOnNotifiableEvent,
|
||||||
coroutineDispatchers = testCoroutineDispatchers(),
|
coroutineDispatchers = testCoroutineDispatchers(),
|
||||||
json = DefaultJsonProvider(),
|
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun TestScope.createWorkerParams(
|
private fun TestScope.createWorkerParams(
|
||||||
|
|
|
||||||
|
|
@ -58,5 +58,5 @@ private fun createSyncNotificationWorkManagerRequest(
|
||||||
) = SyncNotificationWorkManagerRequest(
|
) = SyncNotificationWorkManagerRequest(
|
||||||
sessionId = sessionId,
|
sessionId = sessionId,
|
||||||
notificationEventRequests = notificationEventRequests,
|
notificationEventRequests = notificationEventRequests,
|
||||||
json = DefaultJsonProvider(),
|
workerDataConverter = WorkerDataConverter(DefaultJsonProvider()),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.push.impl.workmanager
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.androidutils.json.DefaultJsonProvider
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||||
|
import io.element.android.libraries.matrix.test.AN_EVENT_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||||
|
import io.element.android.libraries.push.api.push.NotificationEventRequest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class WorkerDataConverterTest {
|
||||||
|
@Test
|
||||||
|
fun `ensure identity when serializing - deserializing an empty list`() {
|
||||||
|
testIdentity(emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ensure identity when serializing - deserializing a list`() {
|
||||||
|
testIdentity(
|
||||||
|
listOf(
|
||||||
|
NotificationEventRequest(
|
||||||
|
sessionId = A_SESSION_ID,
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
providerInfo = "info1",
|
||||||
|
),
|
||||||
|
NotificationEventRequest(
|
||||||
|
sessionId = A_SESSION_ID_2,
|
||||||
|
roomId = A_ROOM_ID_2,
|
||||||
|
eventId = AN_EVENT_ID_2,
|
||||||
|
providerInfo = "info2",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun testIdentity(data: List<NotificationEventRequest>) {
|
||||||
|
val sut = WorkerDataConverter(DefaultJsonProvider())
|
||||||
|
val serialized = sut.serialize(data).getOrThrow()
|
||||||
|
val result = sut.deserialize(serialized)
|
||||||
|
assertThat(result).isEqualTo(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue