From f7a562842ccb33eaeb391f8ba450fdbeec68893e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Forlini?= Date: Mon, 4 May 2026 11:08:16 +0200 Subject: [PATCH 1/5] Bump to 7.9.7 NOJIRA --- app-configuration.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app-configuration.properties b/app-configuration.properties index 1a96aa3bb9..208f918dde 100644 --- a/app-configuration.properties +++ b/app-configuration.properties @@ -5,4 +5,4 @@ targetSdk=35 ndkVersion=28.1.13356709 testInstrumentationRunner=ch.protonmail.android.uitest.HiltTestRunner versionCode=1 -versionName=7.9.6 +versionName=7.9.7 From 21e35683fd6b02680a61d52946cfd99d70f4839f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Forlini?= Date: Mon, 4 May 2026 11:08:10 +0200 Subject: [PATCH 2/5] Bump Rust SDK to 0.164.22 NOJIRA --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e9b6f81995..d1fb1d41f9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,7 +69,7 @@ material = "1.13.0" mockk = "1.14.7" paparazzi = "1.3.5" proton-core = "36.2.0" -proton-rust-core = "0.164.20" +proton-rust-core = "0.164.22" robolectric = "4.15.1" # 4.16.0 requires Java 21 kotlinpoet-ksp = "2.2.0" leakcanary = "2.14" From d99cc04e3cd41788f681975cd403da49d4a55c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Forlini?= Date: Fri, 24 Apr 2026 10:25:05 +0200 Subject: [PATCH 3/5] Update BackgroundExecutionWorkScheduler logic - Do not require batteryNotLow (gated behind remote FF) - Ensure that scheduling happens before onExitForeground is called - Add additional debug logging ET-6144 --- .../background/RustWorkLifecycleObserver.kt | 8 ++-- .../mailfeatureflags/di/FeatureFlagsModule.kt | 13 +++++ .../annotation/FeatureFlagAnnotations.kt | 4 ++ .../model/FeatureFlagDefinitionItems.kt | 8 ++++ .../BackgroundExecutionWorkScheduler.kt | 21 +++++++- .../BackgroundExecutionWorkSchedulerTest.kt | 48 ++++++++++++++++++- 6 files changed, 95 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt b/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt index d148a16b59..142b72ecd4 100644 --- a/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt +++ b/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt @@ -41,9 +41,11 @@ class RustWorkLifecycleObserver @Inject constructor( } override fun onStop(owner: LifecycleOwner) { - onRustExitForeground() - backgroundExecutionWorkScheduler.scheduleWork() - Timber.d("onStop finished - onExitForeground + schedule work called") + owner.lifecycleScope.launch { + backgroundExecutionWorkScheduler.scheduleWork() + onRustExitForeground() + Timber.d("onStop finished - schedule work called + onExitForeground") + } } private fun onRustExitForeground() { diff --git a/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt b/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt index 62e4f7966c..1d80caf37f 100644 --- a/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt +++ b/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt @@ -25,6 +25,7 @@ import ch.protonmail.android.mailfeatureflags.data.local.factory.BooleanFeatureF import ch.protonmail.android.mailfeatureflags.domain.FeatureFlagValueProvider import ch.protonmail.android.mailfeatureflags.domain.annotation.ComposerAutoCollapseQuotedTextEnabled import ch.protonmail.android.mailfeatureflags.domain.annotation.FeatureFlagsCoroutineScope +import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingNewConstraintEnabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBlackFridayWave1Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBlackFridayWave2Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsDebugInspectDbEnabled @@ -39,6 +40,7 @@ import ch.protonmail.android.mailfeatureflags.domain.annotation.IsShowRatingBoos import ch.protonmail.android.mailfeatureflags.domain.annotation.IsSpringOffer2026Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsSpringOffer2026Wave2Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsUpsellEnabled +import ch.protonmail.android.mailfeatureflags.domain.model.BgProcessingNewConstraint import ch.protonmail.android.mailfeatureflags.domain.model.ComposerAutoCollapseQuotedText import ch.protonmail.android.mailfeatureflags.domain.model.ConversationDetailAutoExpandLastMessageEnabled import ch.protonmail.android.mailfeatureflags.domain.model.DebugInspectDbEnabled @@ -229,4 +231,15 @@ object FeatureFlagsModule { @Singleton fun providePushProcessingWithoutWorkerDef(): FeatureFlagDefinition = PushProcessingWithoutWorker + @Provides + @Singleton + @IsBgProcessingNewConstraintEnabled + fun provideBgProcessingNewConstraintEnabled(factory: BooleanFeatureFlagFactory) = + factory.create(BgProcessingNewConstraint.key, false) + + @Provides + @IntoSet + @Singleton + fun provideBgProcessingNewConstraintDef(): FeatureFlagDefinition = BgProcessingNewConstraint + } diff --git a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt index 4b43bc6892..3f966834fc 100644 --- a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt +++ b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt @@ -79,3 +79,7 @@ annotation class IsFeatureSpotlightEnabled @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IsPushProcessingWithoutWorkerEnabled + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class IsBgProcessingNewConstraintEnabled diff --git a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt index 848dff285f..53a23945f9 100644 --- a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt +++ b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt @@ -137,3 +137,11 @@ data object PushProcessingWithoutWorker : FeatureFlagDefinition( description = "Do not schedule a worker to process notifications, but process them directly when received.", defaultValue = false ) + +data object BgProcessingNewConstraint : FeatureFlagDefinition( + key = "MailAndroidV7BgProcessingNewConstraint", + name = "Relax battery-not-low constraint for background processing", + category = FeatureFlagCategory.Global, + description = "When enabled, the periodic background execution worker does not require the battery to be not low.", + defaultValue = false +) diff --git a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt index 0e70d8149f..bd22a4a452 100644 --- a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt +++ b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt @@ -18,23 +18,40 @@ package ch.protonmail.android.mailsession.data.background +import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType import ch.protonmail.android.mailcommon.data.worker.CancelWorkManagerWork import ch.protonmail.android.mailcommon.data.worker.Enqueuer +import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingNewConstraintEnabled +import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag +import timber.log.Timber import javax.inject.Inject class BackgroundExecutionWorkScheduler @Inject constructor( private val enqueuer: Enqueuer, - private val cancelWorkManagerWork: CancelWorkManagerWork + private val cancelWorkManagerWork: CancelWorkManagerWork, + @IsBgProcessingNewConstraintEnabled private val bgProcessingNewConstraintEnabled: FeatureFlag ) { - fun scheduleWork() { + suspend fun scheduleWork() { + val requiresBatteryNotLow = !bgProcessingNewConstraintEnabled.get() + Timber.d("Scheduling background work with requiresBatteryNotLow=$requiresBatteryNotLow") + + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(requiresBatteryNotLow) + .build() + enqueuer.enqueueUniquePeriodicWork( workerId = WORKER_ID, tag = BACKGROUND_WORK_TAG, worker = BackgroundExecutionWorker::class.java, + constraints = constraints, existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE ) + + Timber.d("Background periodic work scheduled.") } suspend fun cancelPendingWork() { diff --git a/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt b/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt index 5222c73927..7947417f25 100644 --- a/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt +++ b/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt @@ -18,9 +18,12 @@ package ch.protonmail.android.mailsession.data.background +import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType import ch.protonmail.android.mailcommon.data.worker.CancelWorkManagerWork import ch.protonmail.android.mailcommon.data.worker.Enqueuer +import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag import io.mockk.called import io.mockk.coEvery import io.mockk.coVerify @@ -34,19 +37,23 @@ internal class BackgroundExecutionWorkSchedulerTest { private val enqueuer = mockk() private val cancelWorkManagerWork = mockk() + private val bgProcessingNewConstraintEnabled = mockk>() private val backgroundScheduler = BackgroundExecutionWorkScheduler( enqueuer, - cancelWorkManagerWork + cancelWorkManagerWork, + bgProcessingNewConstraintEnabled ) @Test - fun `should enqueue the periodic work once requested`() = runTest { + fun `should enqueue periodic work requiring battery not low when FF is off`() = runTest { // Given + coEvery { bgProcessingNewConstraintEnabled.get() } returns false coEvery { enqueuer.enqueueUniquePeriodicWork( workerId = any(), tag = BACKGROUND_WORK_TAG, worker = BackgroundExecutionWorker::class.java, + constraints = any(), existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE ) } just runs @@ -60,6 +67,38 @@ internal class BackgroundExecutionWorkSchedulerTest { workerId = DEFAULT_WORKER_ID, tag = BACKGROUND_WORK_TAG, worker = BackgroundExecutionWorker::class.java, + constraints = expectedConstraints(requiresBatteryNotLow = true), + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + + cancelWorkManagerWork wasNot called + } + } + + @Test + fun `should enqueue periodic work ignoring battery not low when FF is on`() = runTest { + // Given + coEvery { bgProcessingNewConstraintEnabled.get() } returns true + coEvery { + enqueuer.enqueueUniquePeriodicWork( + workerId = any(), + tag = BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = any(), + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + } just runs + + // When + backgroundScheduler.scheduleWork() + + // Then + coVerify(exactly = 1) { + enqueuer.enqueueUniquePeriodicWork( + workerId = DEFAULT_WORKER_ID, + tag = BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = expectedConstraints(requiresBatteryNotLow = false), existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE ) @@ -82,6 +121,11 @@ internal class BackgroundExecutionWorkSchedulerTest { } } + private fun expectedConstraints(requiresBatteryNotLow: Boolean): Constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(requiresBatteryNotLow) + .build() + private companion object { const val BACKGROUND_WORK_TAG = "background_work_execution" From 49488bfdbb6c3ce90e763b8c17a540b6744a5a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Forlini?= Date: Mon, 27 Apr 2026 14:43:49 +0200 Subject: [PATCH 4/5] Further tweak Background worker scheduling Avoid resolving the FF value within onStop and delegate to a different worker ET-6144 --- .../background/RustWorkLifecycleObserver.kt | 8 +- .../RustWorkLifecycleObserverTest.kt | 5 +- .../mailcommon/data/worker/Enqueuer.kt | 13 ++ .../BackgroundExecutionWorkScheduler.kt | 34 ++--- .../ScheduleBackgroundExecutionWorker.kt | 69 +++++++++ .../BackgroundExecutionWorkSchedulerTest.kt | 90 +++--------- .../ScheduleBackgroundExecutionWorkerTest.kt | 132 ++++++++++++++++++ 7 files changed, 251 insertions(+), 100 deletions(-) create mode 100644 mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt create mode 100644 mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorkerTest.kt diff --git a/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt b/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt index 142b72ecd4..13ed3f5275 100644 --- a/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt +++ b/app/src/main/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserver.kt @@ -41,11 +41,9 @@ class RustWorkLifecycleObserver @Inject constructor( } override fun onStop(owner: LifecycleOwner) { - owner.lifecycleScope.launch { - backgroundExecutionWorkScheduler.scheduleWork() - onRustExitForeground() - Timber.d("onStop finished - schedule work called + onExitForeground") - } + backgroundExecutionWorkScheduler.scheduleWork() + onRustExitForeground() + Timber.d("onStop finished - schedule work called + onExitForeground") } private fun onRustExitForeground() { diff --git a/app/src/test/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserverTest.kt b/app/src/test/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserverTest.kt index dccb0a689e..c9da4be0bd 100644 --- a/app/src/test/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserverTest.kt +++ b/app/src/test/kotlin/ch/protonmail/android/initializer/background/RustWorkLifecycleObserverTest.kt @@ -30,6 +30,7 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import io.mockk.verify import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import me.proton.core.test.kotlin.TestDispatcherProvider @@ -67,7 +68,7 @@ internal class RustWorkLifecycleObserverTest { @Test fun `should schedule background execution and pause work when onStop is triggered`() = runTest { // Given - coEvery { scheduler.scheduleWork() } just runs + every { scheduler.scheduleWork() } just runs every { mailSessionRepository.getMailSession().onExitForeground() } just runs val lifecycleOwner = TestLifecycleOwner(Lifecycle.State.CREATED, dispatcher) @@ -76,7 +77,7 @@ internal class RustWorkLifecycleObserverTest { advanceUntilIdle() // Then - coVerify(exactly = 1) { scheduler.scheduleWork() } + verify(exactly = 1) { scheduler.scheduleWork() } coVerify(exactly = 1) { mailSessionRepository.getMailSession().onExitForeground() } confirmVerified(mailSessionRepository, scheduler) } diff --git a/mail-common/data/src/main/kotlin/ch/protonmail/android/mailcommon/data/worker/Enqueuer.kt b/mail-common/data/src/main/kotlin/ch/protonmail/android/mailcommon/data/worker/Enqueuer.kt index f480463611..6262fe7751 100644 --- a/mail-common/data/src/main/kotlin/ch/protonmail/android/mailcommon/data/worker/Enqueuer.kt +++ b/mail-common/data/src/main/kotlin/ch/protonmail/android/mailcommon/data/worker/Enqueuer.kt @@ -195,6 +195,19 @@ class Enqueuer @Inject constructor(private val workManager: WorkManager) { ) } + fun enqueueUniqueWork( + workerId: String, + worker: Class, + existingWorkPolicy: ExistingWorkPolicy = ExistingWorkPolicy.REPLACE, + constraints: Constraints? = null + ) { + val request = OneTimeWorkRequest.Builder(worker).apply { + if (constraints != null) setConstraints(constraints) + }.build() + + workManager.enqueueUniqueWork(workerId, existingWorkPolicy, request) + } + fun enqueueUniquePeriodicWork( workerId: String, worker: Class, diff --git a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt index bd22a4a452..b772a9c624 100644 --- a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt +++ b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkScheduler.kt @@ -18,47 +18,33 @@ package ch.protonmail.android.mailsession.data.background -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType +import androidx.work.ExistingWorkPolicy import ch.protonmail.android.mailcommon.data.worker.CancelWorkManagerWork import ch.protonmail.android.mailcommon.data.worker.Enqueuer -import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingNewConstraintEnabled -import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag import timber.log.Timber import javax.inject.Inject class BackgroundExecutionWorkScheduler @Inject constructor( private val enqueuer: Enqueuer, - private val cancelWorkManagerWork: CancelWorkManagerWork, - @IsBgProcessingNewConstraintEnabled private val bgProcessingNewConstraintEnabled: FeatureFlag + private val cancelWorkManagerWork: CancelWorkManagerWork ) { - suspend fun scheduleWork() { - val requiresBatteryNotLow = !bgProcessingNewConstraintEnabled.get() - Timber.d("Scheduling background work with requiresBatteryNotLow=$requiresBatteryNotLow") - - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(requiresBatteryNotLow) - .build() - - enqueuer.enqueueUniquePeriodicWork( - workerId = WORKER_ID, - tag = BACKGROUND_WORK_TAG, - worker = BackgroundExecutionWorker::class.java, - constraints = constraints, - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + fun scheduleWork() { + enqueuer.enqueueUniqueWork( + workerId = ScheduleBackgroundExecutionWorker.WORKER_ID, + worker = ScheduleBackgroundExecutionWorker::class.java, + existingWorkPolicy = ExistingWorkPolicy.REPLACE ) - Timber.d("Background periodic work scheduled.") + Timber.d("Schedule background execution worker enqueued.") } suspend fun cancelPendingWork() { + enqueuer.cancelWork(ScheduleBackgroundExecutionWorker.WORKER_ID) cancelWorkManagerWork.cancelAllWorkByTag(BACKGROUND_WORK_TAG) } - private companion object { + internal companion object { const val BACKGROUND_WORK_TAG = "background_work_execution" const val WORKER_ID = "background_work_execution_task" diff --git a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt new file mode 100644 index 0000000000..fb286c59c9 --- /dev/null +++ b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package ch.protonmail.android.mailsession.data.background + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.NetworkType +import androidx.work.WorkerParameters +import ch.protonmail.android.mailcommon.data.worker.Enqueuer +import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingNewConstraintEnabled +import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import timber.log.Timber + +@HiltWorker +internal class ScheduleBackgroundExecutionWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val enqueuer: Enqueuer, + @IsBgProcessingNewConstraintEnabled private val bgProcessingNewConstraintEnabled: FeatureFlag +) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result = runCatching { + val requiresBatteryNotLow = !bgProcessingNewConstraintEnabled.get() + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(requiresBatteryNotLow) + .build() + + enqueuer.enqueueUniquePeriodicWork( + workerId = BackgroundExecutionWorkScheduler.WORKER_ID, + tag = BackgroundExecutionWorkScheduler.BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = constraints, + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + + Timber.d("Periodic work enqueued with requiresBatteryNotLow=$requiresBatteryNotLow") + Result.success() + }.getOrElse { throwable -> + Timber.w(throwable, "Scheduling failed; will retry.") + Result.retry() + } + + internal companion object { + + const val WORKER_ID = "schedule_background_execution" + } +} diff --git a/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt b/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt index 7947417f25..9e0cd549c5 100644 --- a/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt +++ b/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/BackgroundExecutionWorkSchedulerTest.kt @@ -18,18 +18,16 @@ package ch.protonmail.android.mailsession.data.background -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType +import androidx.work.ExistingWorkPolicy import ch.protonmail.android.mailcommon.data.worker.CancelWorkManagerWork import ch.protonmail.android.mailcommon.data.worker.Enqueuer -import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag -import io.mockk.called import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.runs +import io.mockk.verify import kotlinx.coroutines.test.runTest import kotlin.test.Test @@ -37,24 +35,19 @@ internal class BackgroundExecutionWorkSchedulerTest { private val enqueuer = mockk() private val cancelWorkManagerWork = mockk() - private val bgProcessingNewConstraintEnabled = mockk>() private val backgroundScheduler = BackgroundExecutionWorkScheduler( enqueuer, - cancelWorkManagerWork, - bgProcessingNewConstraintEnabled + cancelWorkManagerWork ) @Test - fun `should enqueue periodic work requiring battery not low when FF is off`() = runTest { + fun `should enqueue scheduler worker with replace policy`() { // Given - coEvery { bgProcessingNewConstraintEnabled.get() } returns false - coEvery { - enqueuer.enqueueUniquePeriodicWork( - workerId = any(), - tag = BACKGROUND_WORK_TAG, - worker = BackgroundExecutionWorker::class.java, - constraints = any(), - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + every { + enqueuer.enqueueUniqueWork( + workerId = SCHEDULER_WORKER_ID, + worker = ScheduleBackgroundExecutionWorker::class.java, + existingWorkPolicy = ExistingWorkPolicy.REPLACE ) } just runs @@ -62,73 +55,32 @@ internal class BackgroundExecutionWorkSchedulerTest { backgroundScheduler.scheduleWork() // Then - coVerify(exactly = 1) { - enqueuer.enqueueUniquePeriodicWork( - workerId = DEFAULT_WORKER_ID, - tag = BACKGROUND_WORK_TAG, - worker = BackgroundExecutionWorker::class.java, - constraints = expectedConstraints(requiresBatteryNotLow = true), - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + verify(exactly = 1) { + enqueuer.enqueueUniqueWork( + workerId = SCHEDULER_WORKER_ID, + worker = ScheduleBackgroundExecutionWorker::class.java, + existingWorkPolicy = ExistingWorkPolicy.REPLACE ) - - cancelWorkManagerWork wasNot called } } @Test - fun `should enqueue periodic work ignoring battery not low when FF is on`() = runTest { + fun `should cancel both scheduler and periodic work when requested`() = runTest { // Given - coEvery { bgProcessingNewConstraintEnabled.get() } returns true - coEvery { - enqueuer.enqueueUniquePeriodicWork( - workerId = any(), - tag = BACKGROUND_WORK_TAG, - worker = BackgroundExecutionWorker::class.java, - constraints = any(), - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE - ) - } just runs - - // When - backgroundScheduler.scheduleWork() - - // Then - coVerify(exactly = 1) { - enqueuer.enqueueUniquePeriodicWork( - workerId = DEFAULT_WORKER_ID, - tag = BACKGROUND_WORK_TAG, - worker = BackgroundExecutionWorker::class.java, - constraints = expectedConstraints(requiresBatteryNotLow = false), - existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE - ) - - cancelWorkManagerWork wasNot called - } - } - - @Test - fun `should cancel periodic work when requested`() = runTest { - // Given - coEvery { cancelWorkManagerWork.cancelAllWorkByTag(any()) } just runs + every { enqueuer.cancelWork(SCHEDULER_WORKER_ID) } just runs + coEvery { cancelWorkManagerWork.cancelAllWorkByTag(BACKGROUND_WORK_TAG) } just runs // When backgroundScheduler.cancelPendingWork() // Then - coVerify(exactly = 1) { - enqueuer wasNot called - cancelWorkManagerWork.cancelAllWorkByTag(BACKGROUND_WORK_TAG) - } + verify(exactly = 1) { enqueuer.cancelWork(SCHEDULER_WORKER_ID) } + coVerify(exactly = 1) { cancelWorkManagerWork.cancelAllWorkByTag(BACKGROUND_WORK_TAG) } } - private fun expectedConstraints(requiresBatteryNotLow: Boolean): Constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(requiresBatteryNotLow) - .build() - private companion object { const val BACKGROUND_WORK_TAG = "background_work_execution" - const val DEFAULT_WORKER_ID = "background_work_execution_task" + const val SCHEDULER_WORKER_ID = "schedule_background_execution" } } diff --git a/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorkerTest.kt b/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorkerTest.kt new file mode 100644 index 0000000000..0226566921 --- /dev/null +++ b/mail-session/data/src/test/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorkerTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025 Proton Technologies AG + * This file is part of Proton Technologies AG and Proton Mail. + * + * Proton Mail is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Proton Mail is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Proton Mail. If not, see . + */ + +package ch.protonmail.android.mailsession.data.background + +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ListenableWorker +import androidx.work.NetworkType +import androidx.work.WorkerParameters +import ch.protonmail.android.mailcommon.data.worker.Enqueuer +import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +internal class ScheduleBackgroundExecutionWorkerTest { + + private val enqueuer = mockk() + private val bgProcessingNewConstraintEnabled = mockk>() + private val params = mockk() + + private val worker = ScheduleBackgroundExecutionWorker( + mockk(), + params, + enqueuer, + bgProcessingNewConstraintEnabled + ) + + @Test + fun `should enqueue periodic work requiring battery not low when FF is off`() = runTest { + // Given + coEvery { bgProcessingNewConstraintEnabled.get() } returns false + coEvery { + enqueuer.enqueueUniquePeriodicWork( + workerId = WORKER_ID, + tag = BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = any(), + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + } just runs + + // When + val result = worker.doWork() + + // Then + assertEquals(ListenableWorker.Result.success(), result) + coVerify(exactly = 1) { + enqueuer.enqueueUniquePeriodicWork( + workerId = WORKER_ID, + tag = BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = expectedConstraints(requiresBatteryNotLow = true), + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + } + } + + @Test + fun `should enqueue periodic work ignoring battery not low when FF is on`() = runTest { + // Given + coEvery { bgProcessingNewConstraintEnabled.get() } returns true + coEvery { + enqueuer.enqueueUniquePeriodicWork( + workerId = WORKER_ID, + tag = BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = any(), + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + } just runs + + // When + val result = worker.doWork() + + // Then + assertEquals(ListenableWorker.Result.success(), result) + coVerify(exactly = 1) { + enqueuer.enqueueUniquePeriodicWork( + workerId = WORKER_ID, + tag = BACKGROUND_WORK_TAG, + worker = BackgroundExecutionWorker::class.java, + constraints = expectedConstraints(requiresBatteryNotLow = false), + existingPeriodicWorkPolicy = ExistingPeriodicWorkPolicy.CANCEL_AND_REENQUEUE + ) + } + } + + @Test + fun `should retry when feature flag lookup fails`() = runTest { + // Given + coEvery { bgProcessingNewConstraintEnabled.get() } throws IllegalStateException("boom") + + // When + val result = worker.doWork() + + // Then + assertEquals(ListenableWorker.Result.retry(), result) + } + + private fun expectedConstraints(requiresBatteryNotLow: Boolean): Constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(requiresBatteryNotLow) + .build() + + private companion object { + + const val BACKGROUND_WORK_TAG = "background_work_execution" + const val WORKER_ID = "background_work_execution_task" + } +} From 36eb85256b3acd254b11f07d2cf769fc63495c8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Forlini?= Date: Mon, 27 Apr 2026 14:49:37 +0200 Subject: [PATCH 5/5] Rename BG exec battery-related FF value ET-6144 --- .../android/mailfeatureflags/di/FeatureFlagsModule.kt | 10 +++++----- .../domain/annotation/FeatureFlagAnnotations.kt | 2 +- .../domain/model/FeatureFlagDefinitionItems.kt | 4 ++-- .../background/ScheduleBackgroundExecutionWorker.kt | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt b/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt index 1d80caf37f..a40c3c826f 100644 --- a/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt +++ b/mail-featureflags/dagger/src/main/kotlin/ch/protonmail/android/mailfeatureflags/di/FeatureFlagsModule.kt @@ -25,7 +25,7 @@ import ch.protonmail.android.mailfeatureflags.data.local.factory.BooleanFeatureF import ch.protonmail.android.mailfeatureflags.domain.FeatureFlagValueProvider import ch.protonmail.android.mailfeatureflags.domain.annotation.ComposerAutoCollapseQuotedTextEnabled import ch.protonmail.android.mailfeatureflags.domain.annotation.FeatureFlagsCoroutineScope -import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingNewConstraintEnabled +import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingRelaxedBatteryConstraintEnabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBlackFridayWave1Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBlackFridayWave2Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsDebugInspectDbEnabled @@ -40,7 +40,7 @@ import ch.protonmail.android.mailfeatureflags.domain.annotation.IsShowRatingBoos import ch.protonmail.android.mailfeatureflags.domain.annotation.IsSpringOffer2026Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsSpringOffer2026Wave2Enabled import ch.protonmail.android.mailfeatureflags.domain.annotation.IsUpsellEnabled -import ch.protonmail.android.mailfeatureflags.domain.model.BgProcessingNewConstraint +import ch.protonmail.android.mailfeatureflags.domain.model.BgProcessingRelaxedBatteryConstraint import ch.protonmail.android.mailfeatureflags.domain.model.ComposerAutoCollapseQuotedText import ch.protonmail.android.mailfeatureflags.domain.model.ConversationDetailAutoExpandLastMessageEnabled import ch.protonmail.android.mailfeatureflags.domain.model.DebugInspectDbEnabled @@ -233,13 +233,13 @@ object FeatureFlagsModule { @Provides @Singleton - @IsBgProcessingNewConstraintEnabled + @IsBgProcessingRelaxedBatteryConstraintEnabled fun provideBgProcessingNewConstraintEnabled(factory: BooleanFeatureFlagFactory) = - factory.create(BgProcessingNewConstraint.key, false) + factory.create(BgProcessingRelaxedBatteryConstraint.key, false) @Provides @IntoSet @Singleton - fun provideBgProcessingNewConstraintDef(): FeatureFlagDefinition = BgProcessingNewConstraint + fun provideBgProcessingNewConstraintDef(): FeatureFlagDefinition = BgProcessingRelaxedBatteryConstraint } diff --git a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt index 3f966834fc..5c9f755431 100644 --- a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt +++ b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/annotation/FeatureFlagAnnotations.kt @@ -82,4 +82,4 @@ annotation class IsPushProcessingWithoutWorkerEnabled @Qualifier @Retention(AnnotationRetention.BINARY) -annotation class IsBgProcessingNewConstraintEnabled +annotation class IsBgProcessingRelaxedBatteryConstraintEnabled diff --git a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt index 53a23945f9..6636672e25 100644 --- a/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt +++ b/mail-featureflags/domain/src/main/kotlin/ch/protonmail/android/mailfeatureflags/domain/model/FeatureFlagDefinitionItems.kt @@ -138,8 +138,8 @@ data object PushProcessingWithoutWorker : FeatureFlagDefinition( defaultValue = false ) -data object BgProcessingNewConstraint : FeatureFlagDefinition( - key = "MailAndroidV7BgProcessingNewConstraint", +data object BgProcessingRelaxedBatteryConstraint : FeatureFlagDefinition( + key = "MailAndroidV7BgProcessingRelaxedConstraints", name = "Relax battery-not-low constraint for background processing", category = FeatureFlagCategory.Global, description = "When enabled, the periodic background execution worker does not require the battery to be not low.", diff --git a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt index fb286c59c9..7460a59f1a 100644 --- a/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt +++ b/mail-session/data/src/main/kotlin/ch/protonmail/android/mailsession/data/background/ScheduleBackgroundExecutionWorker.kt @@ -26,7 +26,7 @@ import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType import androidx.work.WorkerParameters import ch.protonmail.android.mailcommon.data.worker.Enqueuer -import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingNewConstraintEnabled +import ch.protonmail.android.mailfeatureflags.domain.annotation.IsBgProcessingRelaxedBatteryConstraintEnabled import ch.protonmail.android.mailfeatureflags.domain.model.FeatureFlag import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -37,7 +37,7 @@ internal class ScheduleBackgroundExecutionWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val enqueuer: Enqueuer, - @IsBgProcessingNewConstraintEnabled private val bgProcessingNewConstraintEnabled: FeatureFlag + @IsBgProcessingRelaxedBatteryConstraintEnabled private val bgProcessingNewConstraintEnabled: FeatureFlag ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result = runCatching {