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 9492977c3d..cff12cc17d 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 @@ -29,30 +29,29 @@ class SyncNotificationWorkManagerRequest( private val workerDataConverter: WorkerDataConverter, private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, ) : WorkManagerRequest { - override fun build(): Result { + override fun build(): List> { if (notificationEventRequests.isEmpty()) { - return Result.failure(InvalidParameterException("notificationEventRequests cannot be empty")) - } - val data = workerDataConverter.serialize(notificationEventRequests).getOrElse { - return Result.failure(it) + return listOf(Result.failure(InvalidParameterException("notificationEventRequests cannot be empty"))) } Timber.d("Scheduling ${notificationEventRequests.size} notification requests with WorkManager for $sessionId") - return Result.success( - OneTimeWorkRequestBuilder() - .setInputData(data) - .apply { - // Expedited workers aren't needed on Android 12 or lower: - // They force displaying a foreground sync notification for no good reason, since they sync almost immediately anyway - // See https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat - if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { - setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + return workerDataConverter.serialize(notificationEventRequests).map { + it.map { data -> + OneTimeWorkRequestBuilder() + .setInputData(data) + .apply { + // Expedited workers aren't needed on Android 12 or lower: + // They force displaying a foreground sync notification for no good reason, since they sync almost immediately anyway + // See https://developer.android.com/develop/background-work/background-tasks/persistent/getting-started/define-work#backwards-compat + if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) { + setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) + } } - } - .setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC)) - // TODO investigate using this instead of the resolver queue - // .setInputMerger() - .build() - ) + .setTraceTag(workManagerTag(sessionId, WorkManagerRequestType.NOTIFICATION_SYNC)) + // TODO investigate using this instead of the resolver queue + // .setInputMerger() + .build() + } + } } @Serializable 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/WorkerDataConverter.kt index ac51c6c6a9..71d24cd62d 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/WorkerDataConverter.kt @@ -12,6 +12,7 @@ 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.mapCatchingExceptions 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 @@ -23,12 +24,29 @@ import timber.log.Timber class WorkerDataConverter( private val json: JsonProvider, ) { - fun serialize(notificationEventRequests: List): Result { + fun serialize(notificationEventRequests: List): List> { + return serializeRequests(notificationEventRequests) + .fold( + onSuccess = { + listOf(Result.success(it)) + }, + onFailure = { + // Perform serialization on sublists, workDataOf may have failed because of size limit + Timber.w(it, "Failed to serialize ${notificationEventRequests.size} notification requests, trying with chunks of $CHUNK_SIZE.") + notificationEventRequests.chunked(CHUNK_SIZE).map { chunk -> + serializeRequests(chunk) + } + }, + ) + } + + private fun serializeRequests(notificationEventRequests: List): Result { return runCatchingExceptions { json().encodeToString(notificationEventRequests.map { it.toData() }) } .onFailure { Timber.e(it, "Failed to serialize notification requests") } - .map { str -> + .mapCatchingExceptions { str -> + // Note: workDataOf can fail if the data is too large workDataOf(REQUESTS_KEY to str) } } @@ -51,6 +69,7 @@ class WorkerDataConverter( companion object { private const val REQUESTS_KEY = "requests" + private const val CHUNK_SIZE = 20 } } diff --git a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerRequest.kt b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerRequest.kt index 7c3b165102..a6e4a26e3d 100644 --- a/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerRequest.kt +++ b/libraries/workmanager/api/src/main/kotlin/io/element/android/libraries/workmanager/api/WorkManagerRequest.kt @@ -11,5 +11,5 @@ package io.element.android.libraries.workmanager.api import androidx.work.WorkRequest interface WorkManagerRequest { - fun build(): Result + fun build(): List> } 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 70c9b719b2..6b19e48208 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 @@ -27,14 +27,16 @@ class DefaultWorkManagerScheduler( private val workManager by lazy { WorkManager.getInstance(context) } override fun submit(workManagerRequest: WorkManagerRequest) { - workManagerRequest.build().fold( - onSuccess = { - workManager.enqueue(it) - }, - onFailure = { - Timber.e(it, "Failed to build WorkManager request $workManagerRequest") - } - ) + workManagerRequest.build().forEach { + it.fold( + onSuccess = { workRequest -> + workManager.enqueue(workRequest) + }, + onFailure = { + Timber.e(it, "Failed to build WorkManager request $workManagerRequest") + } + ) + } } override fun cancel(sessionId: SessionId) {